summaryrefslogtreecommitdiffhomepage
path: root/packages/kernel/src/contracts/tool.ts
blob: d5a835c69827494ea37e1417bd4750665a6d9626 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
/**
 * Tool contract — what a tool conforms to and what the kernel calls.
 *
 * The kernel never finds or names a concrete tool; it receives them via
 * `runTurn` and dispatches by shape. A tool's `parameters` uses a structural
 * JSON-Schema-like type so the kernel stays dependency-light (no zod).
 * Extensions may use zod internally and convert to this shape.
 */

import type { Logger } from "./logging.js";

/**
 * Structural JSON Schema subset for tool parameter declarations.
 * The kernel does not validate against this — the provider serializes it for
 * the model, and the tool implementation validates its own input.
 * Using a structural type (not a library) keeps the kernel dependency-free.
 */
export interface ToolParameterSchema {
	readonly type: "object";
	readonly properties?: Readonly<Record<string, JsonSchemaProperty>>;
	readonly required?: readonly string[];
	readonly additionalProperties?: boolean;
	readonly description?: string;
}

/** A single property within a tool's parameter schema. */
export interface JsonSchemaProperty {
	readonly type?: string;
	readonly description?: string;
	readonly enum?: readonly string[];
	readonly items?: JsonSchemaProperty;
	readonly properties?: Readonly<Record<string, JsonSchemaProperty>>;
	readonly required?: readonly string[];
	readonly default?: unknown;
}

/**
 * Context passed to a tool's `execute` method. The kernel constructs this per
 * call, attributing streaming output to the specific tool-call id so
 * concurrent tool output is never interleaved ambiguously.
 */
export interface ToolExecuteContext {
	/** Unique id of the tool-call this execution serves. */
	readonly toolCallId: string;

	/**
	 * Stream output from the tool. The kernel attributes every call to the
	 * tool-call id, so concurrent shell output from different tools is
	 * correctly separated.
	 */
	readonly onOutput: (data: string, stream: "stdout" | "stderr") => void;

	/**
	 * Cancellation signal. An aborted turn sets this so in-flight tool work
	 * can clean up rather than leak.
	 */
	readonly signal: AbortSignal;

	/**
	 * Pre-bound Logger scoped to this tool-call span. Tools log correlated
	 * without a global (P3). The kernel stamps extensionId, conversationId,
	 * turnId, and spanId automatically.
	 */
	readonly log: Logger;

	/**
	 * Working directory for this turn, forwarded verbatim from `RunTurnInput.cwd`.
	 * Tools that touch the filesystem resolve and contain paths against it.
	 * Optional: when omitted, a tool falls back to its own configured/default
	 * workdir. The kernel never interprets it.
	 */
	readonly cwd?: string;

	/**
	 * The conversation this tool-call belongs to. Tools that maintain
	 * per-conversation state (e.g. a todo list) key on this. Forwarded
	 * verbatim from `RunTurnInput.conversationId`. Optional: when omitted,
	 * a tool has no conversation scope (e.g. a global tool).
	 */
	readonly conversationId?: string;
}

/**
 * The value a tool returns from execution. Content is a string for
 * provider-agnostic transport; `isError` flags failure so the model can
 * react without the kernel interpreting the content.
 */
export interface ToolResult {
	readonly content: string;
	readonly isError?: boolean;
}

/**
 * A tool-call as emitted by the provider and dispatched by the kernel.
 * The kernel matches `name` against registered tools and passes `input`
 * to the matched tool's `execute`.
 */
export interface ToolCall {
	readonly id: string;
	readonly name: string;
	readonly input: unknown;
}

/**
 * What a tool extension registers with the kernel via `host.defineTool`.
 * The kernel calls `execute` blindly by shape — it never knows which
 * concrete tools exist.
 */
export interface ToolContract {
	/** Unique name the model uses to invoke this tool. */
	readonly name: string;

	/** Human-readable description shown to the model. */
	readonly description: string;

	/** JSON-Schema-ish parameter declaration (structural, no library dep). */
	readonly parameters: ToolParameterSchema;

	/**
	 * Execute the tool with parsed input. The kernel provides a per-call
	 * context (cancellation, output streaming, attribution).
	 */
	readonly execute: (args: unknown, ctx: ToolExecuteContext) => Promise<ToolResult>;

	/**
	 * Whether this tool is safe to run concurrently with other tools.
	 * When `false`, the kernel serializes this tool's calls even when the
	 * dispatch policy allows parallelism. Defaults to `true` if omitted.
	 * This overrides the global setting downward only (never widens parallelism).
	 */
	readonly concurrencySafe?: boolean;
}