Tool skills & Agent runtime
Tool skills extend ordinary Skills so they publish one or more Tools the model can call directly (named call, parameters, result). The Agent runtime is the stable context object Tarsk injects into each Tool skill script’s execute function. The same contract is documented for the agent itself in the bundled tarsk-agent Skill.
Host machinery — MCP discovery, deferral / tool_search, and hook dispatch — stays in the Tarsk CLI. It is not part of this Skill API.
Split: Tool logic (description, parameters, orchestration, formatting) belongs in Skill scripts. The CLI / Agent runtime keeps security and light host surfaces only (cwd/path checks, permission gates, seatbelt, fetch allowlist, DB/network clients, truncate helpers, job registry). Project Tool skill scripts must not import modules — only context.* keeps those gates.
Concepts
Section titled “Concepts”| Term | Meaning |
|---|---|
| Skill | A SKILL.md package (instructions, optional scripts). |
| Tool | A named callable the model invokes with parameters and receives a result from. |
| Tool skill | A Skill whose front matter includes a tools array; it publishes those Tools. |
| Agent runtime | The context argument to execute. Scripts must not import from cli/src/…. |
tarsk-agent | Bundled Skill that documents this interface for agents editing Tool skills. Not a Tool skill itself. |
Package layout
Section titled “Package layout”.agents/skills/my-notes/ # prefer editing the project copy SKILL.md scripts/ notes.js # one file per entry in front matter tools:Tarsk-shipped defaults live under cli/bundled-skills/ with names starting tarsk-. To change one, export it into the Project’s .agents/skills (the tarsk-improve skill’s ensure-exported.ts) and edit that project copy so changes persist. Chat load never creates copies; it only prunes project copies that still match bundled, so untouched packages never linger in the repo. The tarsk- prefix is only the export filter; any Skill with a tools array publishes Tools.
Front matter
Section titled “Front matter”---name: my-notes # must match the directory namedescription: Short trigger text for humans and skill discoveryversion: 1.0.0 # semver; used by exporttools: - notes # model-facing Tool name(s)# deferred-tools: # optional; subset of tools → behind tool_search# - notes---| Field | Required | Description |
|---|---|---|
name | Yes | Must match the skill directory name |
description | Yes | Skill discovery / catalog text |
version | Recommended | Semver for export comparison |
tools | Yes (for Tool skills) | Inventory of Tool names this Skill publishes |
deferred-tools | No | Subset of tools held behind tool_search; omit ⇒ all eager |
tools is not the same as allowed-tools (see Skills → Tool allowlists), which only restricts which existing Tools a Skill may use.
Script contract
Section titled “Script contract”Each scripts/{name}.js (or .ts) must export:
| Export | Type | Role |
|---|---|---|
description | string | Shown to the model for this Tool |
parameters | JSON Schema object | Argument schema |
execute | (args, context, options?) => Promise<result> | Runs the Tool |
Result shape:
{ content: Array<{ type: "text"; text: string }>; details?: unknown;}Throw, or return error text in content, on failure.
options (third argument):
{ signal?: AbortSignal; onUpdate?: (partial) => void }The Tool name comes from the tools array, not from inside the file. A script not listed in tools remains an ordinary skill script (execute_skill_script), not a model Tool.
When to update a Tool skill
Section titled “When to update a Tool skill”Edit the project Tool skill (usually .agents/skills/tarsk-<area>/) when:
- The same tool call fails repeatedly for a fixable reason (bad defaults, weak validation, schema that invites wrong args).
- Reaching the goal took excessive turns or tokens because of tool design (huge dumps instead of
select/ truncation, serial grep→read loops that belong inrun_js, misleading descriptions). - A description or parameter shape no longer matches how the tool should be used.
Prefer a small change to description, parameters, or execute over a one-off workaround in chat. The next tool load picks up the edit. Do not invent host APIs — use context.* below.
What you can change
Section titled “What you can change”| Goal | Approach |
|---|---|
| Change when the model uses a Tool, or its args | Edit description / parameters |
| Pre/post-process around host behavior | Call the matching dedicated context.* API in execute, then format the result |
| New Tool with logic you own | Implement execute with context primitives only (no module imports) |
| Deep change to bash / read / edit / browser / … | Not possible from a Skill — those engines stay behind dedicated context.* host APIs |
Export and resolution
Section titled “Export and resolution”Resolution (per run)
Section titled “Resolution (per run)”- Use Project
.agents/skills/<name>if that folder exists. - Otherwise use bundled
cli/bundled-skills/<name>.
Load never writes. Reload re-resolves and cache-busts ESM imports by script mtime so project edits apply on the next tool load. Project scripts that import/require modules are rejected at load.
Export (deliberate) and prune (chat load)
Section titled “Export (deliberate) and prune (chat load)”Export copies bundled Skills whose names start with tarsk- into .agents/skills. It runs on demand (the tarsk-improve skill’s ensure-exported.ts), never on chat load, and applies version rules:
| Project copy | Action |
|---|---|
| Missing | Copy from bundled |
Same version | Leave alone |
| Bundled major > project major | Overwrite with bundled |
| Otherwise (project ahead, or only minor/patch ahead) | Leave project copy |
Chat load never copies. It only prunes: a project copy byte-identical to bundled is deleted, so the repo keeps only packages that actually differ. Bump version when you publish a deliberate Skill revision. A bundled major bump can overwrite local copies.
Enable and disable
Section titled “Enable and disable”- Tool skills are enabled by default.
- Disabling the Skill (thread skill settings) hides every Tool in its
toolsarray. - Project disabled tools still hide individual Tool names (Bot tool sets still apply).
- Disabling
tarsk-agentdoes not remove Tools; the CLI always injectscontext.
Agent runtime (context) API
Section titled “Agent runtime (context) API”Identifiers
Section titled “Identifiers”| Field | Type | Meaning |
|---|---|---|
cwd | string | Working directory for the run |
projectId | string | Current Project id (may be empty) |
threadId | string | Current Thread id |
conversationId | string | Current Conversation id |
messageId | string? | Optional message id |
threadPath | string? | Optional Thread checkout path |
context.notes
Section titled “context.notes”Project-scoped durable notes:
| Method | Returns |
|---|---|
list() | Note metadata array |
read(name, offset?) | Paged content object, or null |
write(name, content) | Metadata, or null when cleared (empty content) |
append(name, content) | Metadata after append |
search(query) | Name and content excerpts |
context.chat
Section titled “context.chat”| Member | Meaning |
|---|---|
titleMaxLength | Max characters for a chat title |
normalizeTitle(title) | Normalize a model-supplied title for storage |
context.web
Section titled “context.web”context.web.search({ query, numResults?, type?, includeDomains?, excludeDomains?, signal?}): Promise<{ searchType?, requestId?, results: [...] }>context.util
Section titled “context.util”context.util.truncateHead(text, { maxLines?, maxBytes? })context.util.withAbort(signal, (isAborted) => Promise<T>)context.history / context.jobs / context.todos
Section titled “context.history / context.jobs / context.todos”Light host stores: conversation history search/read; owner-scoped job list /
output / kill; thread todos and progress.txt append.
context.fs
Section titled “context.fs”Secure filesystem ops (cwd containment): writeText, glob, applyEdit,
read, grep (all structured where listed; Skill formats model copy).
context.shell / context.ask / context.web.fetch
Section titled “context.shell / context.ask / context.web.fetch”Secure shell (structured run + resolveExit), ask-user wait/normalize, and
HTTP fetch (permission gates + credential store in the host). Truncation /
background / ask / fetch result formatting lives in the Skill.
context.observations
Section titled “context.observations”DB/telemetry only (resolveId, fetch, recordRetrieved, limits). Paging
and miss/continuation copy live in the get_observation Skill.
Other host executors
Section titled “Other host executors”| API | Tool(s) |
|---|---|
context.images.searchAndSave / .generate | find_images, generate_image (Skill formats) |
context.projects.query / .launch | projects, prompt_project (Skill formats) |
context.tasks.query | tasks (Skill formats) |
context.browser.run | browser (Skill formats) |
context.js.run | run_js (Skill formats success; host still throws failure diagnostics) |
context.skillsCatalog.read / .executeScript / .readReference | read_skill, execute_skill_script, read_skill_reference (Skill formats) |
propose_branch runs entirely in its Skill script (no host API) and is only
published for Slack Control Channel plan chat.
Every shipped Tool skill uses a dedicated context.* API (or pure script logic).
Skill scripts own description/parameters and malleable formatting; security and
heavy engines stay in the CLI behind those APIs.
Examples
Section titled “Examples”Custom Tool skill
Section titled “Custom Tool skill”.agents/skills/echo-tool/SKILL.md:
---name: echo-tooldescription: Echoes a message back via a small custom Tool.version: 1.0.0tools: - echo_message---# Echo
Publishes `echo_message`. See the Agent runtime docs or the `tarsk-agent` Skill..agents/skills/echo-tool/scripts/echo_message.js:
export const description = "Echo a short message back to the conversation. Use for smoke-testing Tool skills.";
export const parameters = { type: "object", properties: { message: { type: "string", description: "Text to echo" }, }, required: ["message"],};
export async function execute(args, context) { const text = String(args.message ?? ""); return { content: [{ type: "text", text: `echo (${context.cwd}): ${text}` }], details: undefined, };}Wrapping a host Tool
Section titled “Wrapping a host Tool”After export, override .agents/skills/tarsk-bash/scripts/bash.js:
export const description = "/* revised guidance for the model */";
export const parameters = {/* keep or tighten the JSON Schema */};
export async function execute(args, context, options) { const structured = await context.shell.run(args, options); // Format completed / background / preformatted + resolveExit — see bundled bash.js}Related
Section titled “Related”- Skills — instructional Skills, scripts, and allowlists
- Chat — how Tools appear in conversation
- Permissions — shell and fetch approval (host behavior for
bash/fetch) - Bundled authoring Skill:
tarsk-agent(ships undercli/bundled-skills/; export on demand to edit) - End-of-session improver:
tarsk-improve(user-invoked; exportstarsk-*into.agents/skills, proposes Tool skill patches on those project copies, then prunes unchanged copies)