Skills
A skill is an invokable capability you expose to the model — a tool. Declare its name, description, and a JSON-schema for its arguments; implement one Invoke method.
The interface
public interface ISkill
{
SkillManifest Manifest { get; }
Task<SkillResult> Invoke(SkillContext ctx, CancellationToken ct = default);
}
public sealed record SkillManifest
{
public required string Name { get; init; } // stable id, e.g. "read_file"
public required string Description { get; init; } // one line; enters model context
public JsonElement? Parameters { get; init; } // JSON Schema for the args object
public IReadOnlyList<string> Keywords { get; init; } = []; // for skill discovery/search
}Input & output
SkillContext carries the SessionId, AgentName, the parsed Args (a JsonElement), the CallId, and the agent-settings snapshot taken at turn entry. Return a SkillResult:
SkillResult.Success("done — 3 files changed"); // ok, content goes back to the model
SkillResult.Fail("path not found: /etc/none"); // failure the model can react to
SkillResult.Success(text, attachments); // include AIContent attachmentsA complete skill
The built-in read_file, verbatim — note the schema helper and provider injection:
using AgentParley.Abstractions.Providers;
using AgentParley.Abstractions.Skills;
public sealed class ReadFileSkill(IFilesProvider files) : ISkill
{
public SkillManifest Manifest => new()
{
Name = "read_file",
Description = "Read a file. Output is line-numbered. Optional line range for large files.",
Parameters = Schema.Of(
new Prop("path", "string", "file path (relative to workspace)", true),
new Prop("startLine", "integer", "first line (1-based)"),
new Prop("lineCount", "integer", "number of lines")),
};
public async Task<SkillResult> Invoke(SkillContext ctx, CancellationToken ct = default)
{
var path = Args.Str(ctx.Args, "path", "");
var start = Args.Int(ctx.Args, "startLine") ?? 1;
var count = Args.Int(ctx.Args, "lineCount") ?? 2000;
var content = await files.Read(new FileRef(ctx.AgentName, path), new LineRange(start, count), ct);
return SkillResult.Success(content.Text);
}
}Schema.Of / Prop / Args are convenience helpers in the base plugin. In your own plugin, either mirror that small helper or build the JSON Schema JsonElement directly — the only contract is that Parameters is valid JSON Schema for the arguments object.Registering a skill
Skills usually depend on resolved providers, so register the type in Configure and add the resolved instance in Start:
public void Configure(IPluginContext ctx) =>
ctx.Services.AddSingleton<DeploySkill>();
public Task Start(IPluginRuntime runtime, CancellationToken ct)
{
runtime.Skills.Add(runtime.Services.GetRequiredService<DeploySkill>());
return Task.CompletedTask;
}Each agent's allow/deny policy decides which registered skills it can actually see — adding a skill makes it available, not automatically granted to everyone.
Parking: deferred skills
A skill that needs to wait — for a human answer, a peer reply, or an approval — returns a deferred result. The session parks; when the resolution arrives the skill is re-invoked with ctx.Resolution set, and produces its real result. One skill owns both halves; the runtime never special-cases it.
// first invoke: park until a human answers (the next message is the answer)
return SkillResult.Defer(ctx.CallId, DeferredAwaiting.AwaitingHuman, ResolutionMode.Positional);
// on resume, ctx.Resolution carries the answer:
if (ctx.Resolution is { } r)
return SkillResult.Success($"got it: {r.Text}");| Awaiting | Resolves with |
|---|---|
AwaitingHuman | the next human message (Positional) or a correlated answer |
AwaitingPeer | a correlated reply from another agent |
AwaitingApproval | a correlated approve/deny decision |