@ct-agents/protocol 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tool.ts ADDED
@@ -0,0 +1,275 @@
1
+ import type {
2
+ AgentActionRequestedPayload,
3
+ SessionEvent,
4
+ SessionRecord,
5
+ SessionStore,
6
+ ToolErrorPayload,
7
+ } from './session/index.js';
8
+ import { isSessionEventType as isCoreSessionEventType } from './session/index.js';
9
+ import type { EnvironmentExecutionMode, EnvironmentHostedPolicy, ResourceSlots, SchemaContract } from './environment.js';
10
+ import type { HarnessInfo } from './harness.js';
11
+
12
+ /**
13
+ * 工具对外元数据:供 LLM 决定是否调用、以及前端展示。
14
+ *
15
+ * `inputSchema` / `resultSchema` 为 JSON Schema 形态的描述(面向模型与 UI),
16
+ * 与 {@link ToolHandler.inputSchema} 的运行时校验契约({@link SchemaContract})是两件事:
17
+ * 前者是「声明」,后者是「执行时校验」。
18
+ */
19
+ export type ToolDescriptor = {
20
+ name: string;
21
+ title: string;
22
+ description: string;
23
+ inputSchema: Record<string, unknown>;
24
+ resultSchema: Record<string, unknown>;
25
+ /** 工具执行前必须由 environment 绑定的 resource slot。 */
26
+ requiredResources?: string[];
27
+ annotations: {
28
+ readOnly: boolean;
29
+ idempotent: boolean;
30
+ openWorld: boolean;
31
+ destructive: boolean;
32
+ };
33
+ };
34
+
35
+ /**
36
+ * 工具执行产出的副作用。工具本身不直接写 session 事件,而是通过 effects 声明意图,
37
+ * 由 {@link Environment} 统一落库为 session 事件,保证「工具生命周期事件唯一写入者」。
38
+ */
39
+ export type ToolEffect =
40
+ | { type: 'custom.event'; eventType: string; payload: unknown }
41
+ | { type: 'agent.action.request'; payload: AgentActionRequestedPayload };
42
+
43
+ export type ToolEffectValidationResult =
44
+ | { valid: true; effect: ToolEffect }
45
+ | { valid: false; message: string };
46
+
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
49
+ }
50
+
51
+ function normalizeActionKind(value: unknown): AgentActionRequestedPayload['kind'] | null {
52
+ return value === 'ask_user' || value === 'tool_confirmation' || value === 'custom_tool_result'
53
+ ? value
54
+ : null;
55
+ }
56
+
57
+ function parseExternalActionPayload(value: unknown): ToolEffectValidationResult {
58
+ if (!isRecord(value)) {
59
+ return { valid: false, message: 'agent.action.request.payload 必须是对象' };
60
+ }
61
+ const kind = normalizeActionKind(value.kind);
62
+ if (!kind) {
63
+ return { valid: false, message: 'agent.action.request.payload.kind 无效' };
64
+ }
65
+ if (kind === 'custom_tool_result') {
66
+ return {
67
+ valid: false,
68
+ message: '外部 worker 不能创建 custom_tool_result action;该 action 只能由 hosted app catalog action-backed shim 产生',
69
+ };
70
+ }
71
+ const display = isRecord(value.display)
72
+ ? {
73
+ ...(typeof value.display.title === 'string' ? { title: value.display.title } : {}),
74
+ ...(typeof value.display.summary === 'string' ? { summary: value.display.summary } : {}),
75
+ }
76
+ : undefined;
77
+ return {
78
+ valid: true,
79
+ effect: {
80
+ type: 'agent.action.request',
81
+ payload: {
82
+ kind,
83
+ ...(typeof value.prompt === 'string' ? { prompt: value.prompt } : {}),
84
+ ...(Object.prototype.hasOwnProperty.call(value, 'input') ? { input: value.input } : {}),
85
+ ...(Array.isArray(value.allowedResults) ? { allowedResults: value.allowedResults } : {}),
86
+ ...(display && (display.title || display.summary) ? { display } : {}),
87
+ ...(typeof value.expiresAt === 'string' ? { expiresAt: value.expiresAt } : {}),
88
+ },
89
+ },
90
+ };
91
+ }
92
+
93
+ const RESERVED_SESSION_EVENT_PREFIXES = ['user.', 'assistant.', 'tool.', 'agent.', 'session.'] as const;
94
+
95
+ function isReservedSessionEventNamespace(eventType: string) {
96
+ return RESERVED_SESSION_EVENT_PREFIXES.some((prefix) => eventType.startsWith(prefix));
97
+ }
98
+
99
+ /**
100
+ * 校验跨进程 worker 可回传的 tool effect。
101
+ *
102
+ * 外部 worker 持有环境密钥,但不应能伪造核心生命周期事件,也不能直接创建
103
+ * `custom_tool_result` action。保留 session 事件命名空间也禁止外部 worker 使用,
104
+ * 避免未知核心事件被提前伪造;业务扩展事件应使用非核心命名空间。
105
+ */
106
+ export function parseExternalToolEffect(value: unknown): ToolEffectValidationResult {
107
+ if (!isRecord(value)) {
108
+ return { valid: false, message: 'effect 必须是对象' };
109
+ }
110
+ if (value.type === 'custom.event') {
111
+ const eventType = typeof value.eventType === 'string' ? value.eventType.trim() : '';
112
+ if (!eventType) {
113
+ return { valid: false, message: 'custom.event.eventType 必须是非空字符串' };
114
+ }
115
+ if (isCoreSessionEventType(eventType)) {
116
+ return { valid: false, message: `外部 worker 不能通过 custom.event 写核心事件:${eventType}` };
117
+ }
118
+ if (isReservedSessionEventNamespace(eventType)) {
119
+ return { valid: false, message: `外部 worker 不能通过 custom.event 写保留事件命名空间:${eventType}` };
120
+ }
121
+ return {
122
+ valid: true,
123
+ effect: {
124
+ type: 'custom.event',
125
+ eventType,
126
+ payload: value.payload,
127
+ },
128
+ };
129
+ }
130
+ if (value.type === 'agent.action.request') {
131
+ return parseExternalActionPayload(value.payload);
132
+ }
133
+ return { valid: false, message: 'effect.type 必须是 custom.event 或 agent.action.request' };
134
+ }
135
+
136
+ /** 工具执行完成:携带返回给模型的结果与可选副作用。 */
137
+ export type ToolExecutionCompleted<TResult> = {
138
+ status: 'completed';
139
+ modelResult: TResult;
140
+ effects?: ToolEffect[];
141
+ };
142
+
143
+ /** 工具执行暂停:需要人工 action(如 ask-user / 工具确认)才能继续。 */
144
+ export type ToolExecutionPaused = {
145
+ status: 'paused';
146
+ effects: ToolEffect[];
147
+ };
148
+
149
+ /** 工具执行失败:携带结构化错误,供 session 事件落库与重试决策。 */
150
+ export type ToolExecutionFailed = {
151
+ status: 'failed';
152
+ error: ToolErrorPayload;
153
+ effects?: ToolEffect[];
154
+ };
155
+
156
+ /** 工具执行结果三态:完成 / 暂停(待 action)/ 失败。 */
157
+ export type ToolExecution<TResult> =
158
+ | ToolExecutionCompleted<TResult>
159
+ | ToolExecutionPaused
160
+ | ToolExecutionFailed;
161
+
162
+ /** 工具执行状态字面量,供跨进程 work queue 契约复用。 */
163
+ export type ToolExecutionStatus = ToolExecution<unknown>['status'];
164
+
165
+ /**
166
+ * 工具执行上下文:运行时注入给 handler 的只读环境。
167
+ *
168
+ * - `session` / `harness`:当前会话与 harness 配置快照。
169
+ * - `resources`:按环境配置实例化的资源(数据库 / sandbox 等业务能力)。
170
+ * - `toolCallEvent`:本次调用的 `tool.called` 事件,effects 以其为 parent 落库。
171
+ * - `abortSignal`:用户中断 / 会话停止时触发,handler 应及时终止。
172
+ */
173
+ export type ToolHandlerContext = {
174
+ session: SessionRecord;
175
+ /**
176
+ * 当前会话绑定的 store(scope-free SessionStore),可选。
177
+ *
178
+ * Environment 主流程(executeTool / settleResolvedActions)总会注入;少数无 session 持久化
179
+ * 的调试入口可不传。子 Agent 委派工具据此为子 session 创建/驱动执行(见 §3.4/§4.4 决策 B),
180
+ * 用前应检查非空。普通工具一般不直接写事件——事件写入仍由 Environment 统一负责。
181
+ * 这一字段把「可写 session 的 store」暴露给工具 handler,因此「Environment 唯一写入者」
182
+ * 由结构保证降级为纪律约束(一方工具均为可信代码,可接受)。
183
+ */
184
+ sessionStore?: SessionStore;
185
+ harness: HarnessInfo;
186
+ executionMode: EnvironmentExecutionMode;
187
+ hostedPolicy?: EnvironmentHostedPolicy;
188
+ resources: ResourceSlots;
189
+ toolCallEvent: SessionEvent<'tool.called'>;
190
+ abortSignal?: AbortSignal;
191
+ };
192
+
193
+ /** 工具调用落库前的输入校验上下文;此时尚未创建 `tool.called` 事件。 */
194
+ export type ToolPrepareInputContext = Omit<ToolHandlerContext, 'toolCallEvent'>;
195
+
196
+ /**
197
+ * descriptor 动态解析上下文:orchestrator 在「组装工具列表交给模型」前注入。
198
+ *
199
+ * 仅含 wake 时已就绪、且产出 descriptor 所需的最小快照(当前会话 + harness)。
200
+ * 工具据此把「目录/索引」等会随会话变化的内容注入自身 description(见 {@link ToolHandler.resolveDescriptor})。
201
+ */
202
+ export type ToolDescriptorContext = {
203
+ session: SessionRecord;
204
+ harness: HarnessInfo;
205
+ resources: ResourceSlots;
206
+ };
207
+
208
+ /**
209
+ * 工具恢复执行输入:当暂停态的 action 被用户解决后,重新进入 handler 时携带的上下文。
210
+ *
211
+ * `originalInput` 为首次调用的入参;`actionEvent` / `resolvedEvent` 把 action 的请求与
212
+ * 解决结果成对交还给 handler,使其据此完成后续逻辑。
213
+ */
214
+ export type ToolResumeInput<TInput> = {
215
+ originalInput: TInput;
216
+ actionEvent: SessionEvent<'agent.action.requested'>;
217
+ resolvedEvent: SessionEvent<'user.action.resolved'>;
218
+ };
219
+
220
+ /**
221
+ * 工具实现契约。
222
+ *
223
+ * 一个工具 = 元数据(descriptor)+ 运行时校验(inputSchema / resultSchema)+ 行为(execute / resume)。
224
+ * `inputSchema` / `resultSchema` 是 {@link SchemaContract}:协议只要求「能 parse」,
225
+ * 不绑定具体校验库;实现侧统一使用 zod(其 `ZodType` 结构兼容本契约)。
226
+ *
227
+ * `resume` 可选:声明该工具支持「暂停 → 等待用户 action → 恢复」的交互模式(如 ask-user)。
228
+ * 无 `resume` 的工具被 action 暂停后将无法恢复。
229
+ */
230
+ export interface ToolHandler<TInput = unknown, TResult = unknown> {
231
+ /** 对外元数据,供 LLM 调用决策与前端展示。 */
232
+ descriptor: ToolDescriptor;
233
+ /**
234
+ * 可选:按当前会话/harness 动态产出 descriptor(默认用静态 {@link descriptor})。
235
+ *
236
+ * orchestrator 在把工具列表交给模型前对每个工具调用一次,让工具把「目录/索引」等会随
237
+ * 会话变化的内容注入自身 description(如 load-memory 注入根索引、load-skill 注入关联 Skill 目录)。
238
+ * 实现应自带兜底:解析失败时回退静态 descriptor,不得让单个工具的 digest 失败阻断整次 wake。
239
+ */
240
+ resolveDescriptor?(context: ToolDescriptorContext): Promise<ToolDescriptor>;
241
+ /** 入参运行时校验契约:Environment 在执行前用它校验模型传入的 input。 */
242
+ inputSchema: SchemaContract<TInput>;
243
+ /** 结果运行时校验契约:Environment 在落库 tool.completed 前用它校验 handler 返回值。 */
244
+ resultSchema: SchemaContract<TResult>;
245
+ /**
246
+ * 可选:需要当前 session/app 上下文才能解析 schema 的工具,可在写 `tool.called` 前异步校验输入。
247
+ * 未提供时,Environment 使用同步 `inputSchema.parse`。
248
+ */
249
+ prepareInput?(input: unknown, context: ToolPrepareInputContext): Promise<TInput> | TInput;
250
+ /**
251
+ * 执行工具,返回三态结果(完成 / 暂停 / 失败)。
252
+ * 副作用通过 effects 声明,由 Environment 统一落库;handler 不应直接写 session 事件。
253
+ */
254
+ execute(input: TInput, context: ToolHandlerContext): Promise<ToolExecution<TResult>>;
255
+ /**
256
+ * 恢复暂停的工具。当 action 被 user.action.resolved 解决后由 Environment 调用,
257
+ * 使工具据解决结果完成后续逻辑。仅声明了 resume 的工具可被恢复。
258
+ */
259
+ resume?(input: ToolResumeInput<TInput>, context: ToolHandlerContext): Promise<ToolExecution<TResult>>;
260
+ }
261
+
262
+ /**
263
+ * 工具注册中心:按名称查找工具实现、枚举可用工具元数据。
264
+ *
265
+ * 由 apps/api 装配时填充具体实现(如 InMemoryToolRegistry / CompositeToolRegistry),
266
+ * 运行时(harness / orchestrator)面向本接口编程,不依赖具体实现类。
267
+ */
268
+ export interface ToolRegistry {
269
+ /** 按工具名查找实现;不存在时由实现决定抛错或返回(当前实现抛 ToolNotFoundError)。 */
270
+ getTool(toolName: string): ToolHandler<unknown, unknown>;
271
+ /** 枚举所有已注册工具的对外元数据,供 LLM 工具列表与前端展示。 */
272
+ listTools(): ToolDescriptor[];
273
+ /** 可选:运行时动态追加工具(用于子 Agent 委派工具等按需注册场景)。 */
274
+ addTool?(handler: ToolHandler): void;
275
+ }
@@ -0,0 +1,113 @@
1
+ import type { ToolExecutionResult, ToolWorkItem } from './tool-exec.js';
2
+ import type { EnvironmentExecutionMode } from './environment.js';
3
+
4
+ /** 长轮询认领工作项的输入。按 app + environment 双维度限制 worker 权限。 */
5
+ export interface ClaimWorkItemsInput {
6
+ appId: string;
7
+ environmentId: string;
8
+ /** 最长阻塞毫秒数;实现可按自身传输限制裁剪。 */
9
+ blockMs?: number;
10
+ /** 可选取消信号;仅用于进程内 SPI,不属于 HTTP wire body。 */
11
+ abortSignal?: AbortSignal;
12
+ /** 单次最多认领数量。 */
13
+ max?: number;
14
+ /** 可选执行面过滤;不传时保持旧 worker client 兼容。 */
15
+ executionMode?: EnvironmentExecutionMode;
16
+ }
17
+
18
+ /** lease 过期重认领输入。 */
19
+ export interface ReclaimWorkItemsInput {
20
+ appId: string;
21
+ environmentId: string;
22
+ olderThanMs: number;
23
+ /** 可选执行面过滤;用于 hosted/self-hosted worker 只回收自己执行面的 lease。 */
24
+ executionMode?: EnvironmentExecutionMode;
25
+ }
26
+
27
+ /** 查询工作队列观测指标的输入。 */
28
+ export interface GetWorkStatsInput {
29
+ appId: string;
30
+ environmentId: string;
31
+ /** 可选执行面过滤;用于观测单个执行面的队列深度。 */
32
+ executionMode?: EnvironmentExecutionMode;
33
+ }
34
+
35
+ /** 按调用方 scope 查询单个 work item,用于 resource proxy 等执行面二次校验。 */
36
+ export interface GetWorkItemInput {
37
+ workId: string;
38
+ appId: string;
39
+ environmentId: string;
40
+ executionMode?: EnvironmentExecutionMode;
41
+ }
42
+
43
+ export type WorkItemSnapshotState = 'queued' | 'leased' | 'completed' | 'settled' | 'dead_letter';
44
+
45
+ export interface WorkItemSnapshot {
46
+ item: ToolWorkItem;
47
+ state: WorkItemSnapshotState;
48
+ }
49
+
50
+ /** complete 的调用方已认证范围;HTTP wire body 不携带这些字段,由服务端鉴权层注入。 */
51
+ export interface CompleteWorkItemScope {
52
+ appId: string;
53
+ environmentId: string;
54
+ /** 可选执行面过滤;外部 self-hosted worker 不应能 complete hosted 工作项。 */
55
+ executionMode?: EnvironmentExecutionMode;
56
+ }
57
+
58
+ /** 工作队列观测指标,对齐 Managed Agents work.stats 的核心字段。 */
59
+ export interface WorkStats {
60
+ depth: number;
61
+ pending: number;
62
+ oldestQueuedAt: string | null;
63
+ workersPolling: number;
64
+ }
65
+
66
+ /**
67
+ * 业务 tool 工作队列 SPI。
68
+ *
69
+ * 平台实现负责 enqueue、lease、complete 幂等和超时 reclaim;worker 只通过环境密钥认领
70
+ * 自己 app/environment 的工作项,不能越权读取其他环境或创建 session。
71
+ */
72
+ export interface WorkQueue {
73
+ enqueue(item: ToolWorkItem): Promise<void>;
74
+ claim(input: ClaimWorkItemsInput): Promise<ToolWorkItem[]>;
75
+ /** 返回 true 表示本次结果被接收;同范围内已 completed/settled 也应按幂等成功处理。 */
76
+ complete(result: ToolExecutionResult, scope?: CompleteWorkItemScope): Promise<boolean>;
77
+ reclaim(input: ReclaimWorkItemsInput): Promise<number>;
78
+ stats(input: GetWorkStatsInput): Promise<WorkStats>;
79
+ getItem?(input: GetWorkItemInput): Promise<WorkItemSnapshot | null>;
80
+ }
81
+
82
+ export type ClaimSettlableWorkInput = {
83
+ max?: number;
84
+ };
85
+
86
+ export type ClaimedSettlableWorkItem = {
87
+ item: ToolWorkItem;
88
+ result: ToolExecutionResult;
89
+ settlementAttempts?: number;
90
+ };
91
+
92
+ export type SettlementFailureInput = {
93
+ workId: string;
94
+ error: {
95
+ message: string;
96
+ code?: string;
97
+ details?: unknown;
98
+ };
99
+ deadLetter?: boolean;
100
+ };
101
+
102
+ /**
103
+ * 工具结果结算侧 SPI。
104
+ *
105
+ * 它与 worker 使用的 WorkQueue 分离,避免把平台内部结算方法暴露给 HttpWorkQueueClient。
106
+ */
107
+ export interface WorkSettlementStore {
108
+ claimSettlable(input?: ClaimSettlableWorkInput): Promise<ClaimedSettlableWorkItem[]>;
109
+ /** 返回 false 表示当前实例不再持有结算 lease,调用方不得继续 wake 或计入成功。 */
110
+ markSettled(workId: string): Promise<boolean>;
111
+ /** 返回 false 表示当前实例不再持有结算 lease,失败状态未被可靠记录。 */
112
+ markSettlementFailed(input: SettlementFailureInput): Promise<boolean>;
113
+ }