@ziggs-ai/agent-sdk 0.40.0 → 0.41.0
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/CHANGELOG.md +8 -0
- package/README.md +58 -0
- package/dist/Agent.d.ts +15 -0
- package/dist/Agent.js +73 -15
- package/dist/brain/claudeSdkProtocol.js +1 -1
- package/dist/brain/toolManagerToSdkMcp.js +8 -2
- package/dist/platform/ZiggsEffectHandler.js +30 -14
- package/dist/runtime/executionController.d.ts +60 -0
- package/dist/runtime/executionController.js +165 -0
- package/dist/skills/createSkillAgent.js +15 -1
- package/dist/tools/defineTool.d.ts +6 -0
- package/dist/tools/defineTool.js +1 -0
- package/dist/types.d.ts +7 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -32,6 +32,14 @@
|
|
|
32
32
|
|
|
33
33
|
## Unreleased
|
|
34
34
|
|
|
35
|
+
## 0.41.0
|
|
36
|
+
|
|
37
|
+
- Claude-brain agents run work only under a task they hold. A custom tool, and the platform tools that write work onto a task, need a task assigned to the agent under an active work agreement, verified with the backend on every call; the task's agreement is the permission. The agent names the task in each wake: custom tools gain a `taskId` parameter the model fills and the handler never sees, later calls in the wake keep the task last named, and a task the agent opens for itself in that wake is the one it works under next. A task from an earlier wake is never a default. The host never opens a task and reads nothing off the waking message. Agreeing, managing, reading and talking stay open. Tool results carry the verified binding and operation records are scoped to it. `executionControl` lets other brains opt in or a host opt out; `execution: 'conversation'` keeps a custom tool open. Third-party API and MCP clients are unchanged.
|
|
38
|
+
|
|
39
|
+
## 0.40.1
|
|
40
|
+
|
|
41
|
+
- `createSkillAgent` offers `artifact_record`, `artifact_list`, `artifact_find`, `artifact_share` and `artifact_attach`, so an agent built from instructions can keep its work as artifacts. The file tools that return a storage URL to upload to or fetch from are not offered: the agent has no web tool, and `artifact_record` already takes a file in one call. The set stays fixed in code; instruction text cannot add tools.
|
|
42
|
+
|
|
35
43
|
## 0.40.0
|
|
36
44
|
|
|
37
45
|
- `Agent` names the body returned by `createAgent`; decision engines are `ZiggsBrain` and `ClaudeBrain`. See the README migration table.
|
package/README.md
CHANGED
|
@@ -34,6 +34,64 @@ brain. It returns `{ agent, closeMcp, toolNames }`.
|
|
|
34
34
|
Both implement `Brain`: a `kind` and `tick(TickInput): Promise<TickOutput>`.
|
|
35
35
|
Both use the same agent identity, platform tools, memory and contracts.
|
|
36
36
|
|
|
37
|
+
### Work runs under a task the agent holds
|
|
38
|
+
|
|
39
|
+
`Agent` enables `executionControl` by default for the Claude brain. Other
|
|
40
|
+
brains can enable it with `executionControl: true`; a host that provides its
|
|
41
|
+
own execution policy can set it to `false`. It is a code check in the shared
|
|
42
|
+
tool executor, so it covers nested calls and replayed wakes.
|
|
43
|
+
|
|
44
|
+
The rule: a tool that does work runs only against a task the agent holds, and
|
|
45
|
+
the task's agreement is the permission. Before such a tool runs, the host reads
|
|
46
|
+
the task and its agreement with the agent's own credentials and requires that
|
|
47
|
+
the task is assigned to this agent, that it is active and not paused, stopped,
|
|
48
|
+
held by a dependency or awaiting plan review, and that its agreement is an
|
|
49
|
+
active work agreement. Conversation links and store listings do not authorize
|
|
50
|
+
work. Nothing is read off the message that woke the agent, and the host never
|
|
51
|
+
opens a task on its own.
|
|
52
|
+
|
|
53
|
+
The agent names the task in each wake. Every custom tool gets a `taskId`
|
|
54
|
+
parameter the model fills; the handler never receives it. Later calls in the
|
|
55
|
+
same wake keep working under the task last named, and a task the agent opens
|
|
56
|
+
for itself with `task_create` in that wake is the task it works under next.
|
|
57
|
+
The lane keeps the task in its state so the model sees it next time, never as
|
|
58
|
+
a default: a task from an earlier wake has to be named again. Each call
|
|
59
|
+
verifies the task again, so a pause, a stop or a revocation stops the next
|
|
60
|
+
call.
|
|
61
|
+
|
|
62
|
+
Agreeing, managing, reading and talking stay open: proposing, countering,
|
|
63
|
+
responding, claiming, fulfilling and revoking agreements, opening or cancelling
|
|
64
|
+
tasks, listing, reading context, opening conversations, scheduling a wake and
|
|
65
|
+
sending messages need no task here. Their backend permissions still apply. The
|
|
66
|
+
platform tools that write work onto a task, `artifact_record`,
|
|
67
|
+
`task_set_result`, `task_update_plan_step` and `task_replace_plan`, need the
|
|
68
|
+
task they name. Updating the plan or setting the result of a paused or held
|
|
69
|
+
task is allowed; working on it is not.
|
|
70
|
+
|
|
71
|
+
Successful work tool results include an `execution` object with the verified
|
|
72
|
+
`agreementId` and `taskId`. Tool handlers receive `executionTaskId` and
|
|
73
|
+
`serviceAgreementId` in their context, and operation records carry both ids.
|
|
74
|
+
The agent still submits the actual result through `task_set_result`. Blocked
|
|
75
|
+
calls return a stable `errorCode` and an explanation naming the next step; the
|
|
76
|
+
Claude MCP bridge presents these as a structured error instead of invoking the
|
|
77
|
+
tool.
|
|
78
|
+
|
|
79
|
+
A tool author can keep a custom tool open when it only explains the service or
|
|
80
|
+
performs other conversation-only operations:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
defineTool('describe_service', {}, async () => 'I prepare project briefs.', {
|
|
84
|
+
execution: 'conversation',
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use `execution: 'work'` to require a task for a tool otherwise treated as
|
|
89
|
+
open. These classifications are host configuration, not model arguments. The
|
|
90
|
+
controller governs tools this host runs; it cannot classify reply text,
|
|
91
|
+
translate agreement terms into permissions, or constrain tools a third-party
|
|
92
|
+
agent runs elsewhere. Backend grants remain authoritative for resources,
|
|
93
|
+
connections, delegation and spending.
|
|
94
|
+
|
|
37
95
|
```ts
|
|
38
96
|
import { createAgent, claudeBrain } from '@ziggs-ai/agent-sdk';
|
|
39
97
|
|
package/dist/Agent.d.ts
CHANGED
|
@@ -109,6 +109,8 @@ export interface AgentOptions {
|
|
|
109
109
|
* Internal mapping: `fsm` → Ziggs brain, `claude-sdk` → Claude brain.
|
|
110
110
|
*/
|
|
111
111
|
cognition?: CognitionKind;
|
|
112
|
+
/** Code-enforced work agreement/task binding. Defaults on for the Claude brain; other brains can opt in. */
|
|
113
|
+
executionControl?: boolean;
|
|
112
114
|
/**
|
|
113
115
|
* Spend a model turn on an activation notice for a hire an agent formed.
|
|
114
116
|
* Default off — the host records a no-op and waits for the assigned task.
|
|
@@ -279,6 +281,19 @@ export declare class Agent {
|
|
|
279
281
|
constructor(options?: AgentOptions);
|
|
280
282
|
private _validate;
|
|
281
283
|
private _setup;
|
|
284
|
+
private executionControlOn;
|
|
285
|
+
/**
|
|
286
|
+
* The door every work tool passes: the agent names a task it holds, the
|
|
287
|
+
* host verifies it with the backend, and the task's agreement is the
|
|
288
|
+
* permission. Reads use the agent's own credentials in this lane.
|
|
289
|
+
*/
|
|
290
|
+
private executionController;
|
|
291
|
+
/**
|
|
292
|
+
* The lane's task and agreement after a wake, as the controller saw them.
|
|
293
|
+
* Kept so the model sees them in its context next time and can name the
|
|
294
|
+
* task again; the next wake never takes them as a default.
|
|
295
|
+
*/
|
|
296
|
+
private carryExecutionState;
|
|
282
297
|
/** Runtime control is a task fact, never model-interpreted message text. */
|
|
283
298
|
private taskExecutionControl;
|
|
284
299
|
private acknowledgeExecutionControl;
|
package/dist/Agent.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createDeclaredPollRunner } from './platform/polls/declaredPollRunner.js
|
|
|
5
5
|
import { randomUUID } from 'node:crypto';
|
|
6
6
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
7
7
|
import { contextScopesFromWake, WakeEffects, workScopeFromWake } from './runtime/wakeEffects.js';
|
|
8
|
+
import { ExecutionController, asWorkTool, executionControllers } from './runtime/executionController.js';
|
|
8
9
|
export { workScopeFromWake } from './runtime/wakeEffects.js';
|
|
9
10
|
import { ZiggsBrain } from './brain/ZiggsBrain.js';
|
|
10
11
|
import { ClaudeBrain } from './brain/ClaudeBrain.js';
|
|
@@ -23,7 +24,7 @@ import { WAKE_TOOLS } from './platform/tools/tier1/wakeTools.js';
|
|
|
23
24
|
import { createZiggsEffectHandler } from './platform/ZiggsEffectHandler.js';
|
|
24
25
|
import { normalizeIncomingEvent } from './ingress/normalizeIncoming.js';
|
|
25
26
|
import { PromptBuilder } from './runtime/PromptBuilder.js';
|
|
26
|
-
import { removeTaskBook } from './runtime/taskBookkeeping.js';
|
|
27
|
+
import { ensureTaskBook, removeTaskBook } from './runtime/taskBookkeeping.js';
|
|
27
28
|
import { hireIdsFromEvent, memorySectionForWake, partitionMemoryStore, uniqueHireIds, } from './memory/MemoryStore.js';
|
|
28
29
|
import { memoryStoreFor } from './memory/ArtifactMemoryStore.js';
|
|
29
30
|
import { ToolManager } from './tools/ToolManager.js';
|
|
@@ -32,7 +33,7 @@ import { PROTOCOL_TOOLS } from './platform/tools/tier1/protocolTools.js';
|
|
|
32
33
|
import { marketplaceViewTool } from './platform/tools/tier2/marketplaceTools.js';
|
|
33
34
|
import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
34
35
|
import { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
|
|
35
|
-
import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient, sendChatMessage, presentSendResult, } from '@ziggs-ai/api-client';
|
|
36
|
+
import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient, sendChatMessage, presentSendResult, getAgreement, getTask, annotationForSdkTool, } from '@ziggs-ai/api-client';
|
|
36
37
|
import { isCheckoutNeed, normalizeNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
|
|
37
38
|
import { openCheckout, closeCheckout } from './checkout/open.js';
|
|
38
39
|
import { resolveBackendUrl } from './shared/backendUrl.js';
|
|
@@ -77,6 +78,19 @@ export function laneSessionIdForTask(task) {
|
|
|
77
78
|
* takes longer than a conversation turn. Its watchdog is at least this.
|
|
78
79
|
*/
|
|
79
80
|
const CHECKOUT_TICK_TIMEOUT_MS = Number(process.env.CLAUDE_SDK_CHECKOUT_TICK_TIMEOUT_MS ?? 40 * 60_000);
|
|
81
|
+
/**
|
|
82
|
+
* A platform tool keeps the platform classification wherever it was handed
|
|
83
|
+
* in (an artifact tool passed through `tools` is still an artifact tool).
|
|
84
|
+
* Anything else is a custom tool, and under execution control a custom tool
|
|
85
|
+
* does work: it gets a `taskId` the model fills (see asWorkTool).
|
|
86
|
+
*/
|
|
87
|
+
const HAND_DEFINED_PLATFORM_TOOLS = new Set([...PROTOCOL_TOOLS, ...CHAT_TOOLS, ...CLAUDE_SDK_TOOLS, ...WAKE_TOOLS, marketplaceViewTool, contextReadTool, openTool, accessExplainTool]
|
|
88
|
+
.map((tool) => toolName(tool)));
|
|
89
|
+
function customToolAsWork(tool) {
|
|
90
|
+
const name = toolName(tool);
|
|
91
|
+
const platform = Boolean(name && (HAND_DEFINED_PLATFORM_TOOLS.has(name) || annotationForSdkTool(name) !== undefined));
|
|
92
|
+
return platform ? tool : asWorkTool(tool);
|
|
93
|
+
}
|
|
80
94
|
function createToolManager(options) {
|
|
81
95
|
const toolManager = new ToolManager();
|
|
82
96
|
const userTools = options.tools || options.services?.tools || [];
|
|
@@ -112,7 +126,8 @@ function createToolManager(options) {
|
|
|
112
126
|
: [];
|
|
113
127
|
const platformTools = [...taskTools, ...CHAT_TOOLS, ...browseTools, ...claudeSdkTools, ...everydayContext, ...WAKE_TOOLS];
|
|
114
128
|
warnOnShadowedPlatformTools(platformTools, ownTools, options.agentId);
|
|
115
|
-
|
|
129
|
+
const controlOn = options.executionControl ?? isClaudeCognition(options.cognition);
|
|
130
|
+
toolManager.registerAll([...platformTools, ...(controlOn ? ownTools.map(customToolAsWork) : ownTools)]);
|
|
116
131
|
return toolManager;
|
|
117
132
|
}
|
|
118
133
|
/**
|
|
@@ -267,6 +282,7 @@ export class Agent {
|
|
|
267
282
|
polls: resolved.polls,
|
|
268
283
|
pollKinds: resolved.pollKinds,
|
|
269
284
|
cognition: resolved.cognition ?? 'fsm',
|
|
285
|
+
executionControl: resolved.executionControl,
|
|
270
286
|
actsOnHireActivation: resolved.actsOnHireActivation === true,
|
|
271
287
|
wakeGate: resolved.wakeGate,
|
|
272
288
|
claudeMaxTurns: resolved.claudeMaxTurns,
|
|
@@ -455,6 +471,43 @@ export class Agent {
|
|
|
455
471
|
});
|
|
456
472
|
});
|
|
457
473
|
}
|
|
474
|
+
executionControlOn() {
|
|
475
|
+
return this.options.executionControl ?? isClaudeCognition(this.options.cognition);
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* The door every work tool passes: the agent names a task it holds, the
|
|
479
|
+
* host verifies it with the backend, and the task's agreement is the
|
|
480
|
+
* permission. Reads use the agent's own credentials in this lane.
|
|
481
|
+
*/
|
|
482
|
+
executionController(identity, signal, onTaskBound) {
|
|
483
|
+
if (!this.executionControlOn())
|
|
484
|
+
return undefined;
|
|
485
|
+
const creds = { operatorKey: this.options.operatorKey, agentId: this.options.agentId, laneId: identity.sessionId };
|
|
486
|
+
return new ExecutionController({
|
|
487
|
+
agentId: identity.agentId, signal, onTaskBound,
|
|
488
|
+
readTask: id => getTask(id, creds),
|
|
489
|
+
readAgreement: id => getAgreement(id, creds),
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The lane's task and agreement after a wake, as the controller saw them.
|
|
494
|
+
* Kept so the model sees them in its context next time and can name the
|
|
495
|
+
* task again; the next wake never takes them as a default.
|
|
496
|
+
*/
|
|
497
|
+
carryExecutionState(ctx, controller) {
|
|
498
|
+
if (!ctx || !controller)
|
|
499
|
+
return;
|
|
500
|
+
const taskId = controller.currentTaskId;
|
|
501
|
+
if (taskId) {
|
|
502
|
+
ensureTaskBook(ctx, taskId, controller.currentAgreementId);
|
|
503
|
+
ctx.activeTaskId = taskId;
|
|
504
|
+
if (controller.currentAgreementId)
|
|
505
|
+
ctx.activeAgreementId = controller.currentAgreementId;
|
|
506
|
+
}
|
|
507
|
+
else if (controller.closedTask && ctx.activeTaskId === controller.closedTask) {
|
|
508
|
+
ctx.activeTaskId = null;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
458
511
|
/** Runtime control is a task fact, never model-interpreted message text. */
|
|
459
512
|
async taskExecutionControl(taskId) {
|
|
460
513
|
if (!this.taskService)
|
|
@@ -1105,6 +1158,7 @@ export class Agent {
|
|
|
1105
1158
|
// event (duplicate sends, runaway per-wake input).
|
|
1106
1159
|
const tickAbort = new AbortController();
|
|
1107
1160
|
const marks = WakeEffects.forWake(frame, session.sessionId);
|
|
1161
|
+
const executionController = this.executionController(identity, tickAbort.signal, taskId => registerTaskRun(taskId));
|
|
1108
1162
|
let heldTask;
|
|
1109
1163
|
const pendingEffects = new Set();
|
|
1110
1164
|
let savedSession;
|
|
@@ -1202,7 +1256,8 @@ export class Agent {
|
|
|
1202
1256
|
};
|
|
1203
1257
|
try {
|
|
1204
1258
|
await checkpoint();
|
|
1205
|
-
|
|
1259
|
+
const tick = () => this.wakeEffects.run(marks, () => this.brain.tick({ event: frame, ctx: session.ctx, state: session.state, identity, eff, memory, transitionTap: this.options.transitionTap, signal: tickAbort.signal, ...(checkout ? { checkout: checkout.session } : {}) }));
|
|
1260
|
+
tickWork = executionController ? executionControllers.run(executionController, tick) : tick();
|
|
1206
1261
|
const result = await Promise.race([
|
|
1207
1262
|
tickWork,
|
|
1208
1263
|
new Promise((_, reject) => {
|
|
@@ -1213,6 +1268,7 @@ export class Agent {
|
|
|
1213
1268
|
}),
|
|
1214
1269
|
]);
|
|
1215
1270
|
session.ctx = result.ctx;
|
|
1271
|
+
this.carryExecutionState(session.ctx, executionController);
|
|
1216
1272
|
session.state = result.state;
|
|
1217
1273
|
this.sessions.set(laneKey, session);
|
|
1218
1274
|
// The box dies with the wake, and what it cost rides the same cost line
|
|
@@ -1440,19 +1496,21 @@ export class Agent {
|
|
|
1440
1496
|
// query outlives the promise that abandoned it and keeps spending.
|
|
1441
1497
|
const abort = new AbortController();
|
|
1442
1498
|
const marks = WakeEffects.forWake(record.event, identity.sessionId);
|
|
1499
|
+
const controller = this.executionController(identity, abort.signal);
|
|
1443
1500
|
let watchdog;
|
|
1444
1501
|
try {
|
|
1502
|
+
const tick = () => this.wakeEffects.run(marks, () => this.brain.tick({
|
|
1503
|
+
event: record.event,
|
|
1504
|
+
ctx: record.ctx,
|
|
1505
|
+
state: record.state,
|
|
1506
|
+
identity,
|
|
1507
|
+
eff: this.eff,
|
|
1508
|
+
memory,
|
|
1509
|
+
transitionTap: this.options.transitionTap,
|
|
1510
|
+
signal: abort.signal,
|
|
1511
|
+
}));
|
|
1445
1512
|
const result = await Promise.race([
|
|
1446
|
-
|
|
1447
|
-
event: record.event,
|
|
1448
|
-
ctx: record.ctx,
|
|
1449
|
-
state: record.state,
|
|
1450
|
-
identity,
|
|
1451
|
-
eff: this.eff,
|
|
1452
|
-
memory,
|
|
1453
|
-
transitionTap: this.options.transitionTap,
|
|
1454
|
-
signal: abort.signal,
|
|
1455
|
-
})),
|
|
1513
|
+
controller ? executionControllers.run(controller, tick) : tick(),
|
|
1456
1514
|
new Promise((_, reject) => {
|
|
1457
1515
|
watchdog = setTimeout(() => {
|
|
1458
1516
|
abort.abort(new Error('tick timeout'));
|
|
@@ -1621,7 +1679,7 @@ export class Agent {
|
|
|
1621
1679
|
* shutdown log) instead of EventQueue printing a stray warning.
|
|
1622
1680
|
*/
|
|
1623
1681
|
drain(timeoutMs = 30_000) { return this.eventQueue.waitForIdle(timeoutMs); }
|
|
1624
|
-
registerTool(tool) { return this.toolManager.register(tool); }
|
|
1682
|
+
registerTool(tool) { return this.toolManager.register(this.executionControlOn() ? customToolAsWork(tool) : tool); }
|
|
1625
1683
|
isConnected() { return this.started && (!this.inboxLoopEnabled || (this.inboxLoop?.running ?? false)); }
|
|
1626
1684
|
/**
|
|
1627
1685
|
* Whether this agent is still reading its inbox, and when it last did. One
|
|
@@ -22,7 +22,7 @@ Your prose and final summary are private host notes, not messages to the people
|
|
|
22
22
|
|
|
23
23
|
- The event in your prompt is your work order. Use \`context_snapshot\` when you need chat history, agreement details, or participants beyond what the event carries.
|
|
24
24
|
- For factual answers, read relevant source artifacts before concluding information is missing. The snapshot's artifact list is a bounded inventory, not document contents; use \`context_read\` on the room's artifacts and follow pagination, or search artifacts when that tool is available. An empty direct \`grant_list\` is not evidence that room documents are unavailable. Report dated information with its date and distinguish it from a fresh confirmation. Check shared sources before repeating another agent's unsupported "no data" conclusion.
|
|
25
|
-
- Work is a task under an agreement (the ticket). Post progress with \`task_update_plan_step\`.
|
|
25
|
+
- Work is a task under an agreement (the ticket). Post progress with \`task_update_plan_step\`. Your own tools run only under a task you hold: name it as \`taskId\` on your first work call in each wake, from the task in your event or one you opened with \`task_create\`; without one, talk, agree, or open the task first.
|
|
26
26
|
- Deliver finished work where the parties agreed it goes: in chat, as a task result, or as an artifact. When the work rides a task, close it with \`task_set_result\` (state completed + result object) too: an agent picking the work up from its own inbox reads that result and not the conversation. Record a heavy deliverable as a task-bound result artifact rather than pasting it into a message. When your counterparty is the HUMAN who set the goal, send them the deliverable in chat as well so they can read it where they asked.
|
|
27
27
|
- Closing a task closes the TASK, and only the task. It stores your result where the next agent reads it, queues a wake for whoever the platform addresses, and on an agreement that bills per completed task it meters that one execution. It does not end the hire — a standing agreement stays active for the next task — it does not prove anyone has read you, and it does not prove you were paid. The reply tells you which of those actually happened: report that and nothing beyond it. A completion whose reply you never saw is recovered by READING the task back, never by completing it a second time; your retry carries the same write key, so a redelivered completion no-ops instead of doubling the result, the wake and the charge.
|
|
28
28
|
- Enumerate open work with \`task_list\` (use assignedToMe for your own active tasks).
|
|
@@ -134,8 +134,14 @@ alwaysLoad = () => true) {
|
|
|
134
134
|
writeKeys: ctx.writeKeys,
|
|
135
135
|
...(ctx.checkout ? { checkout: ctx.checkout } : {}),
|
|
136
136
|
});
|
|
137
|
-
if (r.ok)
|
|
138
|
-
|
|
137
|
+
if (r.ok) {
|
|
138
|
+
const result = toCallToolResult(r.result);
|
|
139
|
+
if (r.execution)
|
|
140
|
+
result.content.push({ type: 'text', text: JSON.stringify({ execution: r.execution }) });
|
|
141
|
+
return result;
|
|
142
|
+
}
|
|
143
|
+
if (r.errorCode)
|
|
144
|
+
return { isError: true, content: [{ type: 'text', text: JSON.stringify({ code: r.errorCode, message: r.error }) }] };
|
|
139
145
|
return toCallToolError(r.error ?? `tool "${name}" failed`);
|
|
140
146
|
}
|
|
141
147
|
catch (err) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { OutboxBuffer } from './OutboxBuffer.js';
|
|
2
2
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
3
|
import { toolFailureText } from '../brain/toolFailureText.js';
|
|
4
|
+
import { ExecutionRefusal, executionControllers } from '../runtime/executionController.js';
|
|
4
5
|
import { artifactScopeForSession } from '@ziggs-ai/api-client';
|
|
5
6
|
import { buildLlmIngestPayload, buildToolIngestPayload, isTelemetryIngestEnabled, sendTelemetry, } from './telemetryIngest.js';
|
|
6
7
|
const CONTEXT_CACHE_MAX = 256;
|
|
@@ -84,7 +85,7 @@ function previewToolArgs(args) {
|
|
|
84
85
|
* Chat timeline: `Message.tsx` parses `entryType: artifact`, `contentType: operation` with
|
|
85
86
|
* `operation_*:{json}` where json includes `state` and `tool`.
|
|
86
87
|
*/
|
|
87
|
-
async function emitToolOperationArtifact(client, sessionId, phase, payload, idempotencyKey, serviceAgreementId) {
|
|
88
|
+
async function emitToolOperationArtifact(client, sessionId, phase, payload, idempotencyKey, serviceAgreementId, taskId) {
|
|
88
89
|
const prefix = phase === 'started' ? 'operation_started' : phase === 'completed' ? 'operation_completed' : 'operation_error';
|
|
89
90
|
const text = `${prefix}:${JSON.stringify(payload)}`;
|
|
90
91
|
try {
|
|
@@ -97,6 +98,7 @@ async function emitToolOperationArtifact(client, sessionId, phase, payload, idem
|
|
|
97
98
|
contentType: 'operation',
|
|
98
99
|
visibility: 'agent-private',
|
|
99
100
|
serviceAgreementId,
|
|
101
|
+
...(taskId ? { taskId } : {}),
|
|
100
102
|
idempotencyKey,
|
|
101
103
|
});
|
|
102
104
|
}
|
|
@@ -122,17 +124,29 @@ async function emitToolOperationArtifact(client, sessionId, phase, payload, idem
|
|
|
122
124
|
*/
|
|
123
125
|
export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
124
126
|
const toolT0 = Date.now();
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
+
let args = effect.args || {};
|
|
128
|
+
const controller = executionControllers.getStore();
|
|
129
|
+
let binding;
|
|
130
|
+
let argsPreview;
|
|
127
131
|
// #6 — breadcrumbs are best-effort. Awaiting them bracketed every
|
|
128
132
|
// tool call; a slow/wedged artifacts backend stalled the whole wake. Match
|
|
129
133
|
// ClaudeBrain thought writes (fire-and-forget).
|
|
130
|
-
void emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'started', {
|
|
131
|
-
state: 'started',
|
|
132
|
-
tool: effect.name,
|
|
133
|
-
...(argsPreview ? { args: argsPreview } : {}),
|
|
134
|
-
}, effect.writeKeys?.artifactKey({ s: 'started', t: effect.name, a: argsPreview }), effect.serviceAgreementId);
|
|
135
134
|
try {
|
|
135
|
+
if (controller) {
|
|
136
|
+
const checked = await controller.beforeTool(deps.toolManager.getTool(effect.name), args);
|
|
137
|
+
args = checked.args;
|
|
138
|
+
binding = checked.binding;
|
|
139
|
+
}
|
|
140
|
+
else if ('taskId' in args && deps.toolManager.getTool(effect.name)?.workTaskParam) {
|
|
141
|
+
// The host added this parameter for the controller; a handler never sees it.
|
|
142
|
+
args = Object.fromEntries(Object.entries(args).filter(([key]) => key !== 'taskId'));
|
|
143
|
+
}
|
|
144
|
+
argsPreview = previewToolArgs(args);
|
|
145
|
+
void emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'started', {
|
|
146
|
+
state: 'started',
|
|
147
|
+
tool: effect.name,
|
|
148
|
+
...(argsPreview ? { args: argsPreview } : {}),
|
|
149
|
+
}, effect.writeKeys?.artifactKey({ s: 'started', t: effect.name, a: argsPreview }), binding?.agreementId ?? effect.serviceAgreementId, binding?.taskId);
|
|
136
150
|
const lastUser = roster.users.find((u) => u.userId || u.id);
|
|
137
151
|
// Accumulate token usage from any nested LLM call the tool makes (llmTool),
|
|
138
152
|
// so it can be folded into the turn's authoritative token count.
|
|
@@ -165,8 +179,9 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
165
179
|
agreements: roster.agreements ?? [],
|
|
166
180
|
senderId: effect.senderId ?? lastUser?.userId ?? lastUser?.id,
|
|
167
181
|
senderType: effect.senderType ?? (lastUser ? 'USER' : undefined),
|
|
168
|
-
onBehalfOfAgreementId: effect.onBehalfOfAgreementId,
|
|
169
|
-
serviceAgreementId: effect.serviceAgreementId,
|
|
182
|
+
onBehalfOfAgreementId: binding?.agreementId ?? effect.onBehalfOfAgreementId,
|
|
183
|
+
serviceAgreementId: binding?.agreementId ?? effect.serviceAgreementId,
|
|
184
|
+
...(binding ? { executionTaskId: binding.taskId, wakeAgreementId: binding.agreementId } : {}),
|
|
170
185
|
memory: effect.memory,
|
|
171
186
|
writeKeys: effect.writeKeys,
|
|
172
187
|
// The wake's checkout, when the host opened one.
|
|
@@ -176,6 +191,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
176
191
|
reportUsage,
|
|
177
192
|
turnScratch: {},
|
|
178
193
|
});
|
|
194
|
+
controller?.observeTool(effect.name.replace(/^(functions|tools|function|tool)\./, ''), result);
|
|
179
195
|
// One clip per tool call: this is a full JSON.stringify of the untruncated
|
|
180
196
|
// result, and the artifact body and the idempotency key want the same string.
|
|
181
197
|
const clipped = clipForOperationArtifact(result);
|
|
@@ -183,7 +199,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
183
199
|
state: 'completed',
|
|
184
200
|
tool: effect.name,
|
|
185
201
|
result: clipped,
|
|
186
|
-
}, effect.writeKeys?.artifactKey({ s: 'completed', t: effect.name, r: clipped }), effect.serviceAgreementId);
|
|
202
|
+
}, effect.writeKeys?.artifactKey({ s: 'completed', t: effect.name, r: clipped }), binding?.agreementId ?? effect.serviceAgreementId, binding?.taskId);
|
|
187
203
|
sendTelemetry(deps.telemetryClient, buildToolIngestPayload({
|
|
188
204
|
sessionId: effect.sessionId,
|
|
189
205
|
tool: effect.name,
|
|
@@ -192,7 +208,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
192
208
|
runId: effect.runId,
|
|
193
209
|
stateId: effect.stateId,
|
|
194
210
|
}), isTelemetryIngestEnabled(deps.telemetryIngest));
|
|
195
|
-
return { ok: true, result, ...(nestedUsage ? { usage: nestedUsage } : {}) };
|
|
211
|
+
return { ok: true, result, ...(binding ? { execution: binding } : {}), ...(nestedUsage ? { usage: nestedUsage } : {}) };
|
|
196
212
|
}
|
|
197
213
|
catch (error) {
|
|
198
214
|
const errMsg = toolFailureText(error);
|
|
@@ -201,7 +217,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
201
217
|
state: 'failed',
|
|
202
218
|
tool: effect.name,
|
|
203
219
|
error: errMsg,
|
|
204
|
-
}, effect.writeKeys?.artifactKey({ s: 'failed', t: effect.name, e: errMsg }), effect.serviceAgreementId);
|
|
220
|
+
}, effect.writeKeys?.artifactKey({ s: 'failed', t: effect.name, e: errMsg }), binding?.agreementId ?? effect.serviceAgreementId, binding?.taskId);
|
|
205
221
|
sendTelemetry(deps.telemetryClient, buildToolIngestPayload({
|
|
206
222
|
sessionId: effect.sessionId,
|
|
207
223
|
tool: effect.name,
|
|
@@ -210,7 +226,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
210
226
|
runId: effect.runId,
|
|
211
227
|
stateId: effect.stateId,
|
|
212
228
|
}), isTelemetryIngestEnabled(deps.telemetryIngest));
|
|
213
|
-
return { ok: false, error: errMsg };
|
|
229
|
+
return { ok: false, error: errMsg, ...(error instanceof ExecutionRefusal ? { errorCode: error.code } : {}) };
|
|
214
230
|
}
|
|
215
231
|
}
|
|
216
232
|
/**
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import type { Task } from '@ziggs-ai/api-client';
|
|
3
|
+
import type { ToolDefinition } from '../tools/defineTool.js';
|
|
4
|
+
type Row = Record<string, unknown>;
|
|
5
|
+
export interface ExecutionBinding {
|
|
6
|
+
agreementId: string;
|
|
7
|
+
taskId: string;
|
|
8
|
+
}
|
|
9
|
+
interface ControllerOptions {
|
|
10
|
+
agentId: string;
|
|
11
|
+
readTask(id: string): Promise<Task>;
|
|
12
|
+
readAgreement(id: string): Promise<unknown>;
|
|
13
|
+
/** Called the first time a task is verified for work in this wake. */
|
|
14
|
+
onTaskBound?(taskId: string): void;
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
export declare const WORK_TASK_PARAM: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* A custom tool does work. It gets a `taskId` the model fills so it can name
|
|
23
|
+
* the task it works under; the host reads it and the handler never sees it.
|
|
24
|
+
* A tool that already takes `taskId` keeps its own, and one the author marked
|
|
25
|
+
* `conversation` stays open.
|
|
26
|
+
*/
|
|
27
|
+
export declare function asWorkTool(tool: ToolDefinition): ToolDefinition;
|
|
28
|
+
export declare class ExecutionRefusal extends Error {
|
|
29
|
+
readonly code: string;
|
|
30
|
+
constructor(code: string, message: string);
|
|
31
|
+
}
|
|
32
|
+
/** Per-wake bookkeeping of which held task the agent works under. Never a replacement for backend grants. */
|
|
33
|
+
export declare class ExecutionController {
|
|
34
|
+
private readonly options;
|
|
35
|
+
private binding?;
|
|
36
|
+
/** A task the agent opened for itself in this wake, not yet verified for work. */
|
|
37
|
+
private named;
|
|
38
|
+
private closedTaskId?;
|
|
39
|
+
constructor(options: ControllerOptions);
|
|
40
|
+
/** The task this wake worked under: the last verified binding, or the task the agent opened for itself. Information for the next wake, never its default. */
|
|
41
|
+
get currentTaskId(): string | null;
|
|
42
|
+
get currentAgreementId(): string | null;
|
|
43
|
+
/** The bound task this wake closed with task_set_result, if any. */
|
|
44
|
+
get closedTask(): string | undefined;
|
|
45
|
+
private refuse;
|
|
46
|
+
private assertLive;
|
|
47
|
+
private isWork;
|
|
48
|
+
beforeTool(tool: ToolDefinition | null, args: Row): Promise<{
|
|
49
|
+
args: Row;
|
|
50
|
+
binding?: ExecutionBinding;
|
|
51
|
+
}>;
|
|
52
|
+
/**
|
|
53
|
+
* Read on every call: a previous successful call is not permission to keep
|
|
54
|
+
* acting after a pause, a stop or a revocation. Read failures fail closed.
|
|
55
|
+
*/
|
|
56
|
+
private verify;
|
|
57
|
+
observeTool(name: string, value: unknown): void;
|
|
58
|
+
}
|
|
59
|
+
export declare const executionControllers: AsyncLocalStorage<ExecutionController>;
|
|
60
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { toolName } from '../tools/defineTool.js';
|
|
3
|
+
import { isListing } from './agreementContext.js';
|
|
4
|
+
const row = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
|
|
5
|
+
const id = (v) => typeof v === 'string' && v.trim() ? v : undefined;
|
|
6
|
+
// The rule: a tool that does work runs only against a task the agent holds,
|
|
7
|
+
// and the task's agreement is the permission. Nothing here reads the wake or
|
|
8
|
+
// a snapshot, and nothing here opens a task: in each wake the agent names a
|
|
9
|
+
// task it was given, or opens one itself with task_create, which the backend
|
|
10
|
+
// prices and the buyer sees. A task from an earlier wake is never a default,
|
|
11
|
+
// so work cannot be filed under last week's task by omission. Agreeing,
|
|
12
|
+
// managing, reading and talking stay open; the backend authorizes each of
|
|
13
|
+
// those on its own.
|
|
14
|
+
/** Platform tools that write work onto a task. Every other platform tool is open. */
|
|
15
|
+
const PLATFORM_WORK = new Set(['artifact_record', 'task_set_result', 'task_update_plan_step', 'task_replace_plan']);
|
|
16
|
+
/** Managing a task you hold needs ownership and a live agreement, not a runnable task: failing a paused task is allowed. */
|
|
17
|
+
const TASK_MANAGEMENT = new Set(['task_set_result', 'task_update_plan_step', 'task_replace_plan']);
|
|
18
|
+
const AUTHORITY_ARGS = ['underAgreementId', 'mandateAgreementId', 'onBehalfOfAgreementId', 'parentAgreementId'];
|
|
19
|
+
const CLOSED = new Set(['completed', 'failed', 'cancelled']);
|
|
20
|
+
const WORKABLE = new Set(['active', 'in-progress']);
|
|
21
|
+
export const WORK_TASK_PARAM = {
|
|
22
|
+
type: 'string',
|
|
23
|
+
description: 'The task this work is for: one assigned to you under an active agreement. Name it on your first work call in each wake; later calls in the same wake keep it.',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A custom tool does work. It gets a `taskId` the model fills so it can name
|
|
27
|
+
* the task it works under; the host reads it and the handler never sees it.
|
|
28
|
+
* A tool that already takes `taskId` keeps its own, and one the author marked
|
|
29
|
+
* `conversation` stays open.
|
|
30
|
+
*/
|
|
31
|
+
export function asWorkTool(tool) {
|
|
32
|
+
if (tool.execution === 'conversation')
|
|
33
|
+
return tool;
|
|
34
|
+
const fn = tool.schema.function;
|
|
35
|
+
const parameters = row(fn.parameters);
|
|
36
|
+
const properties = row(parameters.properties);
|
|
37
|
+
if ('taskId' in properties)
|
|
38
|
+
return { ...tool, execution: 'work' };
|
|
39
|
+
return {
|
|
40
|
+
...tool,
|
|
41
|
+
execution: 'work',
|
|
42
|
+
workTaskParam: true,
|
|
43
|
+
schema: { ...tool.schema, function: { ...fn, parameters: { ...parameters, properties: { ...properties, taskId: WORK_TASK_PARAM } } } },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export class ExecutionRefusal extends Error {
|
|
47
|
+
code;
|
|
48
|
+
constructor(code, message) {
|
|
49
|
+
super(`${code}: ${message}`);
|
|
50
|
+
this.code = code;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Per-wake bookkeeping of which held task the agent works under. Never a replacement for backend grants. */
|
|
54
|
+
export class ExecutionController {
|
|
55
|
+
options;
|
|
56
|
+
binding;
|
|
57
|
+
/** A task the agent opened for itself in this wake, not yet verified for work. */
|
|
58
|
+
named = null;
|
|
59
|
+
closedTaskId;
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.options = options;
|
|
62
|
+
}
|
|
63
|
+
/** The task this wake worked under: the last verified binding, or the task the agent opened for itself. Information for the next wake, never its default. */
|
|
64
|
+
get currentTaskId() {
|
|
65
|
+
if (this.binding && this.binding.taskId !== this.closedTaskId)
|
|
66
|
+
return this.binding.taskId;
|
|
67
|
+
return this.named;
|
|
68
|
+
}
|
|
69
|
+
get currentAgreementId() {
|
|
70
|
+
return this.binding && this.binding.taskId === this.currentTaskId ? this.binding.agreementId : null;
|
|
71
|
+
}
|
|
72
|
+
/** The bound task this wake closed with task_set_result, if any. */
|
|
73
|
+
get closedTask() { return this.closedTaskId; }
|
|
74
|
+
refuse(code, message) { throw new ExecutionRefusal(code, message); }
|
|
75
|
+
assertLive() {
|
|
76
|
+
if (this.options.signal?.aborted)
|
|
77
|
+
this.refuse('EXECUTION_STOPPED', 'This wake has stopped. No new work can execute.');
|
|
78
|
+
}
|
|
79
|
+
isWork(tool, name) {
|
|
80
|
+
if (tool.execution === 'work')
|
|
81
|
+
return true;
|
|
82
|
+
if (tool.execution === 'conversation')
|
|
83
|
+
return false;
|
|
84
|
+
return PLATFORM_WORK.has(name);
|
|
85
|
+
}
|
|
86
|
+
async beforeTool(tool, args) {
|
|
87
|
+
this.assertLive();
|
|
88
|
+
const name = toolName(tool ?? undefined);
|
|
89
|
+
if (!tool || !name)
|
|
90
|
+
this.refuse('UNKNOWN_TOOL', 'The tool is not registered.');
|
|
91
|
+
if (!this.isWork(tool, name))
|
|
92
|
+
return { args };
|
|
93
|
+
const taskId = id(args.taskId) ?? this.binding?.taskId ?? this.named ?? undefined;
|
|
94
|
+
if (!taskId) {
|
|
95
|
+
this.refuse('TASK_REQUIRED', 'Work runs under a task you hold. Name a task assigned to you as taskId, or open one with task_create under your agreement with them and name it.');
|
|
96
|
+
}
|
|
97
|
+
const binding = await this.verify(taskId, TASK_MANAGEMENT.has(name));
|
|
98
|
+
for (const key of AUTHORITY_ARGS) {
|
|
99
|
+
if (args[key] != null && args[key] !== binding.agreementId) {
|
|
100
|
+
this.refuse('AGREEMENT_SCOPE_MISMATCH', `${key} must be the task's own agreement (${binding.agreementId}).`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const handlerArgs = { ...args };
|
|
104
|
+
if (tool.workTaskParam)
|
|
105
|
+
delete handlerArgs.taskId;
|
|
106
|
+
else if ('taskId' in row(row(tool.schema.function.parameters).properties))
|
|
107
|
+
handlerArgs.taskId = binding.taskId;
|
|
108
|
+
return { args: handlerArgs, binding };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Read on every call: a previous successful call is not permission to keep
|
|
112
|
+
* acting after a pause, a stop or a revocation. Read failures fail closed.
|
|
113
|
+
*/
|
|
114
|
+
async verify(taskId, managing) {
|
|
115
|
+
const task = await this.options.readTask(taskId);
|
|
116
|
+
if (task.taskId !== taskId || (task.executorIsYou !== true && task.assigneeId !== this.options.agentId)) {
|
|
117
|
+
this.refuse('TASK_NOT_HELD', `Task ${taskId} is not assigned to you. Work only under a task you hold.`);
|
|
118
|
+
}
|
|
119
|
+
const agreementId = id(task.agreementId);
|
|
120
|
+
const agreement = agreementId ? row(await this.options.readAgreement(agreementId)) : {};
|
|
121
|
+
if (!agreementId || agreement.agreementId !== agreementId || agreement.status !== 'active'
|
|
122
|
+
|| agreement.engagementKind === 'link' || isListing(agreement)) {
|
|
123
|
+
this.refuse('AGREEMENT_INACTIVE', 'The task\'s agreement is not an active work agreement. Conversation links and listings do not authorize work.');
|
|
124
|
+
}
|
|
125
|
+
const state = String(task.state);
|
|
126
|
+
if (CLOSED.has(state) || task.control?.action === 'stop') {
|
|
127
|
+
this.refuse('TASK_NOT_RUNNABLE', `Task ${taskId} is closed or stopped. Take or open another task before working.`);
|
|
128
|
+
}
|
|
129
|
+
const review = row(task.planReview);
|
|
130
|
+
if (!managing && (!WORKABLE.has(state) || task.control?.action === 'pause' || task.heldByDeps || task.heldByParentReview
|
|
131
|
+
|| task.closesBy === 'children' || review.executionAckStatus === 'pending' || review.midWorkAckStatus === 'pending'
|
|
132
|
+
|| task.runIf?.outcome === 'held' || task.runIf?.outcome === 'skipped')) {
|
|
133
|
+
this.refuse('TASK_NOT_RUNNABLE', `Task ${taskId} is paused or awaiting dependencies or review. Resolve that before working on it; you can still update its plan or set its result.`);
|
|
134
|
+
}
|
|
135
|
+
this.assertLive();
|
|
136
|
+
const first = this.binding?.taskId !== taskId;
|
|
137
|
+
this.binding = { agreementId, taskId };
|
|
138
|
+
if (first)
|
|
139
|
+
this.options.onTaskBound?.(taskId);
|
|
140
|
+
return this.binding;
|
|
141
|
+
}
|
|
142
|
+
observeTool(name, value) {
|
|
143
|
+
const result = row(value);
|
|
144
|
+
if (result.ok === false || result.success === false || result.error)
|
|
145
|
+
return;
|
|
146
|
+
const task = row(result.task ?? result);
|
|
147
|
+
const taskId = id(task.taskId);
|
|
148
|
+
if (!taskId)
|
|
149
|
+
return;
|
|
150
|
+
// The agent opened a task for itself: that is the task it means to work
|
|
151
|
+
// under next. It is verified like any other before a tool runs.
|
|
152
|
+
if (name === 'task_create' && (task.assigneeId === this.options.agentId || task.executorIsYou === true))
|
|
153
|
+
this.named = taskId;
|
|
154
|
+
if (name === 'task_set_result' && CLOSED.has(String(task.state))) {
|
|
155
|
+
if (this.binding?.taskId === taskId)
|
|
156
|
+
this.closedTaskId = taskId;
|
|
157
|
+
if (this.named === taskId)
|
|
158
|
+
this.named = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// Covers nested tool calls as well as the brain's direct calls, while keeping
|
|
163
|
+
// concurrent wakes and agents isolated. No controller is mounted for external
|
|
164
|
+
// API/MCP clients; their authorization remains the backend's responsibility.
|
|
165
|
+
export const executionControllers = new AsyncLocalStorage();
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { createAgent } from '../Agent.js';
|
|
2
|
+
import { ARTIFACT_TOOLS } from '../platform/tools/tier2/artifactTools.js';
|
|
3
|
+
import { toolName } from '../tools/defineTool.js';
|
|
2
4
|
import { parseSkill, skillRolePrompt } from './parseSkill.js';
|
|
5
|
+
/** The artifact tools that finish inside one call, so instructions can keep
|
|
6
|
+
* their work as artifacts. The rest hand back a storage URL to upload to or
|
|
7
|
+
* fetch from, and this agent has no web tool to do that with.
|
|
8
|
+
*/
|
|
9
|
+
const SKILL_ARTIFACT_TOOL_NAMES = new Set([
|
|
10
|
+
'artifact_record',
|
|
11
|
+
'artifact_list',
|
|
12
|
+
'artifact_find',
|
|
13
|
+
'artifact_share',
|
|
14
|
+
'artifact_attach',
|
|
15
|
+
]);
|
|
16
|
+
const SKILL_ARTIFACT_TOOLS = ARTIFACT_TOOLS.filter((tool) => SKILL_ARTIFACT_TOOL_NAMES.has(toolName(tool) ?? ''));
|
|
3
17
|
/** The same instructions run on a host or on a person's machine. The trusted
|
|
4
18
|
* caller supplies bytes; instruction text cannot add tools or runtime options.
|
|
5
19
|
*/
|
|
@@ -16,7 +30,7 @@ export function createSkillAgent(options) {
|
|
|
16
30
|
const skill = parseSkill([{ path: 'SKILL.md', bytes: Buffer.from(text, 'utf8') }]);
|
|
17
31
|
return skillRolePrompt(skill);
|
|
18
32
|
},
|
|
19
|
-
tools: [],
|
|
33
|
+
tools: [...SKILL_ARTIFACT_TOOLS],
|
|
20
34
|
taskTools: 'all',
|
|
21
35
|
actsOnHireActivation: true,
|
|
22
36
|
respondsToProposals: true,
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
export interface ToolDefinition {
|
|
2
|
+
/** Host-owned classification. Custom tools do work by default when execution control is enabled. */
|
|
3
|
+
execution?: 'conversation' | 'work';
|
|
4
|
+
/** The host added `taskId` to this tool's parameters so the model can name the task it works under; the handler never receives it. */
|
|
5
|
+
workTaskParam?: boolean;
|
|
2
6
|
schema: {
|
|
3
7
|
type: 'function';
|
|
4
8
|
function: Record<string, unknown>;
|
|
@@ -19,6 +23,8 @@ export interface ToolDefinition {
|
|
|
19
23
|
isGenericFallback?: boolean;
|
|
20
24
|
}
|
|
21
25
|
export interface DefineToolOptions {
|
|
26
|
+
/** Use conversation only for tools safe to run without a work task. This does not grant backend access. */
|
|
27
|
+
execution?: 'conversation' | 'work';
|
|
22
28
|
description?: string;
|
|
23
29
|
echoUserSummaryOnSuccess?: boolean;
|
|
24
30
|
isAgreementCreation?: boolean;
|
package/dist/tools/defineTool.js
CHANGED
|
@@ -74,6 +74,7 @@ export function defineTool(name, params, handler, options = {}) {
|
|
|
74
74
|
return {
|
|
75
75
|
schema: { type: 'function', function: fn },
|
|
76
76
|
handler,
|
|
77
|
+
...(options.execution ? { execution: options.execution } : {}),
|
|
77
78
|
...(options.echoUserSummaryOnSuccess ? { echoUserSummaryOnSuccess: true } : {}),
|
|
78
79
|
...(options.isAgreementCreation ? { isAgreementCreation: true } : {}),
|
|
79
80
|
...(options.isGenericFallback ? { isGenericFallback: true } : {}),
|
package/dist/types.d.ts
CHANGED
|
@@ -389,6 +389,13 @@ export interface ReadContextOpts {
|
|
|
389
389
|
*/
|
|
390
390
|
export interface ToolCallResult {
|
|
391
391
|
ok: boolean;
|
|
392
|
+
/** Stable execution-controller refusal code, when the host blocked the call. */
|
|
393
|
+
errorCode?: string;
|
|
394
|
+
/** Host-verified work binding, supplied by the execution controller. */
|
|
395
|
+
execution?: {
|
|
396
|
+
agreementId: string;
|
|
397
|
+
taskId: string;
|
|
398
|
+
};
|
|
392
399
|
result?: unknown;
|
|
393
400
|
error?: string;
|
|
394
401
|
events?: unknown[];
|