@ziggs-ai/agent-sdk 0.24.1 → 0.25.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 +5 -0
- package/dist/Agent.d.ts +6 -0
- package/dist/Agent.js +34 -3
- package/dist/brain/ClaudeBrain.d.ts +8 -0
- package/dist/brain/ClaudeBrain.js +32 -2
- package/dist/brain/claudeCognition.js +24 -1
- package/dist/brain/claudeSdkProtocol.d.ts +1 -1
- package/dist/brain/claudeSdkProtocol.js +4 -0
- package/dist/brain/claudeSdkTools.js +1 -1
- package/dist/brain/hostedToolSurface.d.ts +14 -0
- package/dist/brain/hostedToolSurface.js +207 -0
- package/dist/brain/wakeGate.d.ts +48 -0
- package/dist/brain/wakeGate.js +76 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
|
|
15
15
|
### Added
|
|
16
16
|
|
|
17
|
+
- The Claude brain asks the backend's wake gate before spending a turn on chat
|
|
18
|
+
messages, and skips the turn (`skippedReason: 'wake_gate_context'`) when the
|
|
19
|
+
answer is a confident "nothing is asked of you". Task, lifecycle and
|
|
20
|
+
notification wakes are never asked about; any failure wakes as before.
|
|
21
|
+
`wakeGate: false` on `AgentOptions` turns it off.
|
|
17
22
|
- `task_cancel` on the hosted Claude surface, same PATCH /tasks/:id/cancel
|
|
18
23
|
as the MCP catalog verb. A held graph withdraws by cancelling the root.
|
|
19
24
|
|
package/dist/Agent.d.ts
CHANGED
|
@@ -113,6 +113,12 @@ export interface AgentOptions {
|
|
|
113
113
|
* A hire a person formed (store claim) still wakes regardless of this flag.
|
|
114
114
|
*/
|
|
115
115
|
actsOnHireActivation?: boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Before a Claude turn on chat messages, ask the backend whether
|
|
118
|
+
* they ask this agent for anything; a confident "no" skips the turn. On by
|
|
119
|
+
* default; `false` turns it off. Any failure wakes as usual.
|
|
120
|
+
*/
|
|
121
|
+
wakeGate?: boolean;
|
|
116
122
|
/** Max agentic turns per Claude SDK wake. Default 30. */
|
|
117
123
|
claudeMaxTurns?: number;
|
|
118
124
|
/**
|
package/dist/Agent.js
CHANGED
|
@@ -7,10 +7,13 @@ import { contextScopesFromWake, WakeEffects, workScopeFromWake } from './runtime
|
|
|
7
7
|
export { workScopeFromWake } from './runtime/wakeEffects.js';
|
|
8
8
|
import { ZiggsBrain } from './brain/ZiggsBrain.js';
|
|
9
9
|
import { ClaudeBrain } from './brain/ClaudeBrain.js';
|
|
10
|
+
import { backendWakeGate } from './brain/wakeGate.js';
|
|
10
11
|
import { applyBrainToAgentOptions, isClaudeCognition, } from './brain/index.js';
|
|
11
12
|
import { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
|
|
12
13
|
import { resolveClaudeModel } from './brain/claudeCognition.js';
|
|
13
14
|
import { CHAT_TOOLS } from './platform/tools/tier2/chatTools.js';
|
|
15
|
+
import { contextReadTool, openTool } from './platform/tools/tier2/contextTools.js';
|
|
16
|
+
import { hostedModelTools } from './brain/hostedToolSurface.js';
|
|
14
17
|
import { EventQueue } from './platform/EventQueue.js';
|
|
15
18
|
import { StateTimers } from './platform/StateTimers.js';
|
|
16
19
|
import { laneKeyFor } from './platform/laneKey.js';
|
|
@@ -92,13 +95,32 @@ function createToolManager(options) {
|
|
|
92
95
|
// chat_open is always-on so claim/provider kickoff (FSM) and the
|
|
93
96
|
// Claude always-on set can open the buyer chat after a store claim. CHAT_TOOLS
|
|
94
97
|
// is also folded into CLAUDE_SDK_TOOLS; Map registration makes duplicates a no-op.
|
|
95
|
-
// wake_at
|
|
96
|
-
//
|
|
98
|
+
// wake_at stays registered for the FSM and for the hosted catalog. The Claude
|
|
99
|
+
// model reaches it through tool_write; an agent that can be handed a task
|
|
97
100
|
// with a check-back rule but cannot arm a wake would have to busy-wait, and
|
|
98
101
|
// there is nothing to busy-wait with.
|
|
99
|
-
|
|
102
|
+
// open / context_read are the hosted twins of the MCP everyday set. Skip
|
|
103
|
+
// them when the agent already brought the same tools, so the shadow warning
|
|
104
|
+
// does not fire on a duplicate registration.
|
|
105
|
+
const ownNames = new Set(ownTools.map((t) => toolName(t)));
|
|
106
|
+
const everydayContext = isClaudeCognition(options.cognition)
|
|
107
|
+
? [contextReadTool, openTool].filter((t) => !ownNames.has(toolName(t)))
|
|
108
|
+
: [];
|
|
109
|
+
const platformTools = [...taskTools, ...CHAT_TOOLS, ...browseTools, ...claudeSdkTools, ...everydayContext, ...WAKE_TOOLS];
|
|
100
110
|
warnOnShadowedPlatformTools(platformTools, ownTools, options.agentId);
|
|
101
111
|
toolManager.registerAll([...platformTools, ...ownTools]);
|
|
112
|
+
// The Claude bridge executes every mounted name through the tool manager,
|
|
113
|
+
// including the catalog. Register those three here or a wake that calls
|
|
114
|
+
// `tools` dies with "Tool not found" before the dispatcher runs.
|
|
115
|
+
if (isClaudeCognition(options.cognition)) {
|
|
116
|
+
const catalog = hostedModelTools([...platformTools, ...ownTools], (name, args, ctx) => toolManager.executeTool(name, args, ctx));
|
|
117
|
+
for (const tool of catalog) {
|
|
118
|
+
const name = toolName(tool);
|
|
119
|
+
if ((name === 'tools' || name === 'tool_read' || name === 'tool_write') && !toolManager.getTool(name)) {
|
|
120
|
+
toolManager.register(tool);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
102
124
|
return toolManager;
|
|
103
125
|
}
|
|
104
126
|
/**
|
|
@@ -250,6 +272,7 @@ export class Agent {
|
|
|
250
272
|
pollKinds: resolved.pollKinds,
|
|
251
273
|
cognition: resolved.cognition ?? 'fsm',
|
|
252
274
|
actsOnHireActivation: resolved.actsOnHireActivation === true,
|
|
275
|
+
wakeGate: resolved.wakeGate,
|
|
253
276
|
claudeMaxTurns: resolved.claudeMaxTurns,
|
|
254
277
|
claudeStatelessPerWake: resolved.claudeStatelessPerWake,
|
|
255
278
|
};
|
|
@@ -405,6 +428,14 @@ export class Agent {
|
|
|
405
428
|
maxTurns: this.options.claudeMaxTurns,
|
|
406
429
|
statelessPerWake: this.options.claudeStatelessPerWake,
|
|
407
430
|
actsOnHireActivation: this.options.actsOnHireActivation === true,
|
|
431
|
+
...(this.options.wakeGate === false || !this.options.operatorKey
|
|
432
|
+
? {}
|
|
433
|
+
: {
|
|
434
|
+
askWakeGate: backendWakeGate({
|
|
435
|
+
operatorKey: this.options.operatorKey,
|
|
436
|
+
agentId: this.options.agentId,
|
|
437
|
+
}),
|
|
438
|
+
}),
|
|
408
439
|
})
|
|
409
440
|
: new ZiggsBrain({
|
|
410
441
|
workflow,
|
|
@@ -4,6 +4,7 @@ import type { Workflow } from '../types.js';
|
|
|
4
4
|
import type { ToolManager } from '../tools/ToolManager.js';
|
|
5
5
|
import { type RawEvent } from '../runtime/buildOutcome.js';
|
|
6
6
|
import type { TickInput, TickOutput } from './ZiggsBrain.js';
|
|
7
|
+
import { type AskWakeGate } from './wakeGate.js';
|
|
7
8
|
export { isPublishProbeChat, isCapabilityProbeSession } from '../probeChat.js';
|
|
8
9
|
/**
|
|
9
10
|
* status notices must not spend a Claude turn answering
|
|
@@ -52,6 +53,12 @@ export interface ClaudeBrainOptions {
|
|
|
52
53
|
* no-op and waits for the task.
|
|
53
54
|
*/
|
|
54
55
|
actsOnHireActivation?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* Asked before a turn is spent on chat messages; a confident
|
|
58
|
+
* "context" skips the turn. Absent means no gate (the default here; the
|
|
59
|
+
* host wires the backend gate in).
|
|
60
|
+
*/
|
|
61
|
+
askWakeGate?: AskWakeGate;
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
57
64
|
* "the transcript you asked me to resume is not here".
|
|
@@ -93,6 +100,7 @@ export declare class ClaudeBrain implements Brain {
|
|
|
93
100
|
private queryFn;
|
|
94
101
|
private readAgreement;
|
|
95
102
|
private actsOnHireActivation;
|
|
103
|
+
private askWakeGate;
|
|
96
104
|
/**
|
|
97
105
|
* Per-lane tool context. One agent object serves every room, and a
|
|
98
106
|
* wake's query can still be running when another lane starts. A single
|
|
@@ -6,6 +6,7 @@ import { getBatchEvents } from '../context/batch.js';
|
|
|
6
6
|
import { bootstrapCtx } from '../runtime/bootstrapCtx.js';
|
|
7
7
|
import { createDeliveryWriteKeys } from '../runtime/deliveryWriteKeys.js';
|
|
8
8
|
import { CLAUDE_SDK_PROTOCOL } from './claudeSdkProtocol.js';
|
|
9
|
+
import { hostedModelTools } from './hostedToolSurface.js';
|
|
9
10
|
import { buildZiggsSdkMcpServer } from './toolManagerToSdkMcp.js';
|
|
10
11
|
import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAssistantText, } from './claudeCognition.js';
|
|
11
12
|
import { assertHostedAgentZiggsTools } from './hostedAgentConnection.js';
|
|
@@ -14,6 +15,7 @@ import { resolveOnBehalfAgreementId, } from './onBehalfAgreement.js';
|
|
|
14
15
|
import { filterStandingOfferNotices, isReadableAgreementNoticeLane } from './standingOfferNotice.js';
|
|
15
16
|
import { formedByFromWake, shouldSkipAgentFormedHireActivation, } from './hireActivationNotice.js';
|
|
16
17
|
import { isPublishProbeChat } from '../probeChat.js';
|
|
18
|
+
import { gateableMessages, wakeAgreementId } from './wakeGate.js';
|
|
17
19
|
export { isPublishProbeChat, isCapabilityProbeSession } from '../probeChat.js';
|
|
18
20
|
/**
|
|
19
21
|
* status notices must not spend a Claude turn answering
|
|
@@ -191,6 +193,7 @@ export class ClaudeBrain {
|
|
|
191
193
|
queryFn;
|
|
192
194
|
readAgreement;
|
|
193
195
|
actsOnHireActivation;
|
|
196
|
+
askWakeGate;
|
|
194
197
|
/**
|
|
195
198
|
* Per-lane tool context. One agent object serves every room, and a
|
|
196
199
|
* wake's query can still be running when another lane starts. A single
|
|
@@ -226,6 +229,7 @@ export class ClaudeBrain {
|
|
|
226
229
|
agentId: this.agentId,
|
|
227
230
|
}));
|
|
228
231
|
this.actsOnHireActivation = opts.actsOnHireActivation === true;
|
|
232
|
+
this.askWakeGate = opts.askWakeGate ?? null;
|
|
229
233
|
}
|
|
230
234
|
async tick({ event, ctx, state, identity, eff, memory, signal }) {
|
|
231
235
|
if (!identity?.sessionId)
|
|
@@ -288,6 +292,31 @@ export class ClaudeBrain {
|
|
|
288
292
|
};
|
|
289
293
|
}
|
|
290
294
|
}
|
|
295
|
+
// Before the breaker counts this wake: a skipped turn costs
|
|
296
|
+
// nothing, so it must not bring the breaker closer. Store publish still
|
|
297
|
+
// has to hear a model reply, so its probe chat is never asked.
|
|
298
|
+
const gateMessages = this.askWakeGate && !isPublishProbeChat(identity.sessionId)
|
|
299
|
+
? gateableMessages(rawEvent)
|
|
300
|
+
: null;
|
|
301
|
+
if (gateMessages && this.askWakeGate) {
|
|
302
|
+
const gate = await this.askWakeGate({
|
|
303
|
+
messages: gateMessages,
|
|
304
|
+
chatId: identity.sessionId,
|
|
305
|
+
...(wakeAgreementId(rawEvent) ? { agreementId: wakeAgreementId(rawEvent) } : {}),
|
|
306
|
+
});
|
|
307
|
+
if (gate && gate.wake === false) {
|
|
308
|
+
runtimeLog.info('ClaudeBrain', `wake gate: context only — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
|
|
309
|
+
`messages=${gateMessages.length} confidence=${gate.confidence ?? '?'}`);
|
|
310
|
+
return {
|
|
311
|
+
ctx: nextCtx,
|
|
312
|
+
state: state ?? DEFAULT_STATE,
|
|
313
|
+
parked: true,
|
|
314
|
+
stateTimeoutUnhandled: false,
|
|
315
|
+
skippedReason: 'wake_gate_context',
|
|
316
|
+
metrics: { llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
}
|
|
291
320
|
const now = Date.now();
|
|
292
321
|
const recentDeliveries = (nextCtx.recentDeliveries ?? []).filter((t) => now - t < WAKE_WINDOW_MS);
|
|
293
322
|
// Store publish still has to hear a model reply when leftover mail already
|
|
@@ -390,7 +419,8 @@ export class ClaudeBrain {
|
|
|
390
419
|
let sdkMcpServer = this.sdkMcpByLane.get(lane);
|
|
391
420
|
if (!sdkMcpServer) {
|
|
392
421
|
const availableTools = await this.toolManager.getAvailableTools();
|
|
393
|
-
|
|
422
|
+
const mounted = hostedModelTools(availableTools, (name, args, ctx) => this.toolManager.executeTool(name, args, ctx));
|
|
423
|
+
sdkMcpServer = buildZiggsSdkMcpServer(mounted, () => {
|
|
394
424
|
const ctx = this.toolContextByLane.get(lane);
|
|
395
425
|
if (!ctx) {
|
|
396
426
|
throw new Error(`ClaudeBrain: tool context missing for lane ${lane}`);
|
|
@@ -398,7 +428,7 @@ export class ClaudeBrain {
|
|
|
398
428
|
return ctx;
|
|
399
429
|
}, {
|
|
400
430
|
name: 'ziggs',
|
|
401
|
-
instructions: 'In-process Ziggs tools
|
|
431
|
+
instructions: 'In-process Ziggs tools. Everyday tools and your own tools are loaded by name. tools lists the rest; tool_read and tool_write call them. agreement_revoke is called by name only.',
|
|
402
432
|
});
|
|
403
433
|
this.sdkMcpByLane.set(lane, sdkMcpServer);
|
|
404
434
|
}
|
|
@@ -99,7 +99,30 @@ export function summarizeContext(snap) {
|
|
|
99
99
|
const agreements = Array.isArray(snap.agreements) ? snap.agreements.length : 0;
|
|
100
100
|
const history = Array.isArray(snap.history) ? snap.history.length : 0;
|
|
101
101
|
const users = Array.isArray(snap.users) ? snap.users.length : 0;
|
|
102
|
-
return `history=${history} messages, agreements=${agreements}, users=${users}${snap.work ? `\nWork context (exact scope, governing constraints and required dependencies):\n${JSON.stringify(snap.work)}` : ''}`;
|
|
102
|
+
return `history=${history} messages, agreements=${agreements}, users=${users}${directArtifactGrantLine(snap)}${snap.work ? `\nWork context (exact scope, governing constraints and required dependencies):\n${JSON.stringify(snap.work)}` : ''}`;
|
|
103
|
+
}
|
|
104
|
+
/** Ids and names only. Absent when the wake has nothing outside the room inventory. */
|
|
105
|
+
function directArtifactGrantLine(snap) {
|
|
106
|
+
const granted = snap.grantedArtifacts;
|
|
107
|
+
if (!granted || typeof granted !== 'object')
|
|
108
|
+
return '';
|
|
109
|
+
const items = granted.items;
|
|
110
|
+
if (!Array.isArray(items) || items.length === 0)
|
|
111
|
+
return '';
|
|
112
|
+
const names = items.flatMap((item) => {
|
|
113
|
+
if (!item || typeof item !== 'object')
|
|
114
|
+
return [];
|
|
115
|
+
const row = item;
|
|
116
|
+
if (typeof row.artifactId !== 'string' || !row.artifactId)
|
|
117
|
+
return [];
|
|
118
|
+
const name = typeof row.name === 'string' && row.name ? ` (${row.name})` : '';
|
|
119
|
+
return [`${row.artifactId}${name}`];
|
|
120
|
+
});
|
|
121
|
+
if (names.length === 0)
|
|
122
|
+
return '';
|
|
123
|
+
const omitted = granted.omitted;
|
|
124
|
+
const more = typeof omitted === 'number' && omitted > 0 ? ` ${omitted} more on grant_list.` : '';
|
|
125
|
+
return `\nReadable artifacts not in this room: ${names.join(', ')}.${more} Bodies are not here; read one with context_read type=artifacts via=artifact:<id>.`;
|
|
103
126
|
}
|
|
104
127
|
/**
|
|
105
128
|
* How a held task's deadline and reminder rule read in the prompt.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLAUDE_SDK_PROTOCOL
|
|
1
|
+
export declare const CLAUDE_SDK_PROTOCOL: string;
|
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
* per-brain; only the hosting surface (tools, effects, events) is shared with
|
|
10
10
|
* the FSM.
|
|
11
11
|
*/
|
|
12
|
+
import { EVERYDAY_SDK_TOOLS } from '@ziggs-ai/api-client';
|
|
12
13
|
import { INBOUND_DISCOVERY_POLICY } from '../inboundDiscoveryPolicy.js';
|
|
14
|
+
const loadedByName = EVERYDAY_SDK_TOOLS.map((name) => `\`${name}\``).join(', ');
|
|
13
15
|
export const CLAUDE_SDK_PROTOCOL = `You are an autonomous agent on a Ziggs team, running as a hosted worker. Your host reads your inbox for you and hands you each event that concerns you — you never poll for news and you never acknowledge anything; the host does both on your behalf. Work waits in your inbox whether your process is running or not, so nothing is ever missed.
|
|
14
16
|
|
|
15
17
|
Each wake hands you ONE event (or a small batch). Handle it, then stop.
|
|
16
18
|
|
|
19
|
+
Ziggs tools loaded with their full schemas are ${loadedByName}, plus any tools that belong to you specifically. Every other Ziggs tool is one call away: \`tools\` lists each one (name and one line); pass \`describe\` to \`tools\` for the schema before you call a tool you have not used. A read goes through \`tool_read\` { tool, args }; a change goes through \`tool_write\` { tool, args }. \`agreement_revoke\` is irreversible, so call it by its own name — the dispatchers refuse it. When the lines below name \`context_snapshot\`, \`wake_at\`, \`task_list\`, \`task_update_plan_step\`, \`grant_list\`, or an agreement verb, that is how you reach it.
|
|
20
|
+
|
|
17
21
|
Your prose and final summary are private host notes, not messages to the people or agents you work with. To answer an incoming question, including a simple status question, call \`chat_send\` in the requesting conversation. Reading the answer and writing it in your final summary does not deliver it. Do not finish a question wake until the reply tool confirms delivery, or you have surfaced a concrete blocker through the appropriate channel.
|
|
18
22
|
|
|
19
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.
|
|
@@ -67,7 +67,7 @@ export const contextSnapshotTool = defineTool('context_snapshot', {
|
|
|
67
67
|
maxMessages: args.maxMessages != null ? Number(args.maxMessages) : DEFAULT_SNAPSHOT_MAX_MESSAGES,
|
|
68
68
|
},
|
|
69
69
|
});
|
|
70
|
-
}, { description: 'Read the current wake’s exact task or agreement, governing constraints, dependencies and artifact inventory. No chat is required for task work. For chat wakes, the NEWEST maxMessages of history (chronological order), agreements, participants, and a bounded inventory of readable room artifacts. Artifact bodies are not included: read relevant notes with context_read type=artifacts via=chat:<chatId>, following pagination, before concluding information is missing. Zero direct artifact grants does not mean no readable room notes.' });
|
|
70
|
+
}, { description: 'Read the current wake’s exact task or agreement, governing constraints, dependencies and artifact inventory. No chat is required for task work. For chat wakes, the NEWEST maxMessages of history (chronological order), agreements, participants, and a bounded inventory of readable room artifacts. Artifact bodies are not included: read relevant notes with context_read type=artifacts via=chat:<chatId>, following pagination, before concluding information is missing. Zero direct artifact grants does not mean no readable room notes. Direct grants absent from that room inventory are grantedArtifacts (id and name only); read a body with context_read type=artifacts via=artifact:<id>.' });
|
|
71
71
|
export const chatSendTool = defineTool('chat_send', {
|
|
72
72
|
taskId: { type: 'string', description: 'Work task; the backend resolves its governing agreement.' },
|
|
73
73
|
replyToMessageId: { type: 'string', description: 'Exact message being answered, preserving its work agreement.' },
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ToolDefinition } from '../tools/defineTool.js';
|
|
2
|
+
export type HostedToolExecute = (name: string, args: Record<string, unknown>, context: Record<string, unknown>) => Promise<unknown>;
|
|
3
|
+
type Lane = 'read-only' | 'write' | 'destructive';
|
|
4
|
+
export declare function hostedToolLane(name: string): Lane;
|
|
5
|
+
/** A Ziggs platform tool the model reaches through the catalog, not by schema. */
|
|
6
|
+
export declare function isHostedCatalogOnly(name: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* The tool block the hosted model is shown.
|
|
9
|
+
*
|
|
10
|
+
* `execute` runs a catalog call through the ToolManager. Omit it when only
|
|
11
|
+
* the schemas are being priced — the dispatchers then refuse to run.
|
|
12
|
+
*/
|
|
13
|
+
export declare function hostedModelTools(all: ToolDefinition[], execute?: HostedToolExecute): ToolDefinition[];
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the hosted Claude brain mounts into the model.
|
|
3
|
+
*
|
|
4
|
+
* The MCP door already lists only the everyday set plus a catalog. This is
|
|
5
|
+
* that same cut for the in-process brain: everyday SDK tools stay loaded by
|
|
6
|
+
* name, agent-owned tools stay loaded by name, and every other Ziggs platform
|
|
7
|
+
* tool is named by `tools` and called through `tool_read` or `tool_write`.
|
|
8
|
+
* The tools stay registered on the ToolManager — the FSM brain and the
|
|
9
|
+
* dispatcher both call them — they just stop riding every wake as schemas.
|
|
10
|
+
*/
|
|
11
|
+
import { EVERYDAY_SDK_TOOLS, annotationForSdkTool } from '@ziggs-ai/api-client';
|
|
12
|
+
import { defineTool, toolName } from '../tools/defineTool.js';
|
|
13
|
+
/**
|
|
14
|
+
* Platform tools written by hand, not as a shared capability. Their lane is
|
|
15
|
+
* what the dispatcher uses; a name here is also a platform name, so it leaves
|
|
16
|
+
* the model block unless it is in the everyday set.
|
|
17
|
+
*/
|
|
18
|
+
const HAND_LANE = {
|
|
19
|
+
context_snapshot: 'read-only',
|
|
20
|
+
chat_send: 'write',
|
|
21
|
+
wake_at: 'write',
|
|
22
|
+
agreement_propose: 'write',
|
|
23
|
+
agreement_subcontract: 'write',
|
|
24
|
+
agreement_respond: 'write',
|
|
25
|
+
agreement_counter: 'write',
|
|
26
|
+
agreement_get: 'read-only',
|
|
27
|
+
agreement_revoke: 'destructive',
|
|
28
|
+
agreement_fulfill: 'write',
|
|
29
|
+
task_set_result: 'write',
|
|
30
|
+
task_update_plan_step: 'write',
|
|
31
|
+
task_create: 'write',
|
|
32
|
+
task_replace_plan: 'write',
|
|
33
|
+
mcp_tools_list: 'read-only',
|
|
34
|
+
mcp_tool_call: 'write',
|
|
35
|
+
connection_list_grants: 'read-only',
|
|
36
|
+
};
|
|
37
|
+
const EVERYDAY = new Set(EVERYDAY_SDK_TOOLS);
|
|
38
|
+
const CATALOG_NAMES = ['tools', 'tool_read', 'tool_write'];
|
|
39
|
+
const CATALOG = new Set(CATALOG_NAMES);
|
|
40
|
+
export function hostedToolLane(name) {
|
|
41
|
+
const hand = HAND_LANE[name];
|
|
42
|
+
if (hand)
|
|
43
|
+
return hand;
|
|
44
|
+
const annotation = annotationForSdkTool(name);
|
|
45
|
+
if (annotation === 'read-only' || annotation === 'destructive')
|
|
46
|
+
return annotation;
|
|
47
|
+
return 'write';
|
|
48
|
+
}
|
|
49
|
+
function isPlatform(name) {
|
|
50
|
+
return name in HAND_LANE || annotationForSdkTool(name) !== undefined;
|
|
51
|
+
}
|
|
52
|
+
/** A Ziggs platform tool the model reaches through the catalog, not by schema. */
|
|
53
|
+
export function isHostedCatalogOnly(name) {
|
|
54
|
+
return isPlatform(name) && !EVERYDAY.has(name);
|
|
55
|
+
}
|
|
56
|
+
function oneLine(description, name) {
|
|
57
|
+
const text = String(description ?? name).replace(/\s+/g, ' ').trim();
|
|
58
|
+
const sentence = text.split(/(?<=\.)\s/)[0] ?? text;
|
|
59
|
+
return sentence.length > 180 ? `${sentence.slice(0, 177)}...` : sentence;
|
|
60
|
+
}
|
|
61
|
+
function callLine(name, lane, native) {
|
|
62
|
+
if (lane === 'destructive') {
|
|
63
|
+
return `call it by name only: ${name}; the dispatchers refuse it`;
|
|
64
|
+
}
|
|
65
|
+
const dispatcher = lane === 'read-only' ? 'tool_read' : 'tool_write';
|
|
66
|
+
if (native) {
|
|
67
|
+
return `call it by name: ${name}; ${dispatcher} { tool: "${name}", args } also works`;
|
|
68
|
+
}
|
|
69
|
+
return `call via ${dispatcher} { tool, args }`;
|
|
70
|
+
}
|
|
71
|
+
function rowFor(tool) {
|
|
72
|
+
const name = toolName(tool) ?? '';
|
|
73
|
+
const lane = hostedToolLane(name);
|
|
74
|
+
const native = !isHostedCatalogOnly(name);
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
line: oneLine(tool.schema.function['description'], name),
|
|
78
|
+
readOnly: lane === 'read-only',
|
|
79
|
+
native,
|
|
80
|
+
call: callLine(name, lane, native),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function refuseWrongLane(name, lane, via) {
|
|
84
|
+
if (lane === 'destructive') {
|
|
85
|
+
throw new Error(`${name} cannot be called through ${via}: it is irreversible, and it is loaded directly so the call names what it does. Call ${name} by name.`);
|
|
86
|
+
}
|
|
87
|
+
const readOnly = lane === 'read-only';
|
|
88
|
+
const askedRead = via === 'tool_read';
|
|
89
|
+
if (readOnly === askedRead)
|
|
90
|
+
return;
|
|
91
|
+
throw new Error(readOnly
|
|
92
|
+
? `${name} is read-only: call it with tool_read { tool, args }, not tool_write.`
|
|
93
|
+
: `${name} changes state: call it with tool_write { tool, args }, not tool_read.`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The Claude bridge does not call a tool's handler. It emits `tool-call`, and
|
|
97
|
+
* the effect handler looks the name up on the ToolManager. A catalog call has
|
|
98
|
+
* to go back through that same effect so the target still gets the timeline,
|
|
99
|
+
* the write keys, and the wake scope. A direct executor is only for a caller
|
|
100
|
+
* that has no effect handler (a schema probe, a unit test).
|
|
101
|
+
*/
|
|
102
|
+
async function callCatalogTarget(name, payload, ctx, execute) {
|
|
103
|
+
const eff = ctx.eff;
|
|
104
|
+
const sessionId = typeof ctx.chatId === 'string' ? ctx.chatId
|
|
105
|
+
: typeof ctx.sessionId === 'string' ? ctx.sessionId
|
|
106
|
+
: undefined;
|
|
107
|
+
if (typeof eff === 'function' && sessionId) {
|
|
108
|
+
const result = await eff({
|
|
109
|
+
kind: 'tool-call',
|
|
110
|
+
sessionId,
|
|
111
|
+
name,
|
|
112
|
+
args: payload,
|
|
113
|
+
senderId: typeof ctx.senderId === 'string' ? ctx.senderId : undefined,
|
|
114
|
+
senderType: typeof ctx.senderType === 'string' ? ctx.senderType : undefined,
|
|
115
|
+
onBehalfOfAgreementId: typeof ctx.onBehalfOfAgreementId === 'string' ? ctx.onBehalfOfAgreementId : undefined,
|
|
116
|
+
serviceAgreementId: typeof ctx.serviceAgreementId === 'string' ? ctx.serviceAgreementId : undefined,
|
|
117
|
+
runId: typeof ctx.runId === 'string' ? ctx.runId : undefined,
|
|
118
|
+
writeKeys: ctx.writeKeys,
|
|
119
|
+
memory: ctx.memory,
|
|
120
|
+
});
|
|
121
|
+
const settled = result;
|
|
122
|
+
if (settled && settled.ok === false)
|
|
123
|
+
throw new Error(settled.error ?? `${name} failed`);
|
|
124
|
+
if (settled && 'ok' in settled)
|
|
125
|
+
return settled.result;
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
if (!execute)
|
|
129
|
+
throw new Error('hosted catalog has no executor');
|
|
130
|
+
return execute(name, payload, ctx);
|
|
131
|
+
}
|
|
132
|
+
function catalogTools(present, execute) {
|
|
133
|
+
const list = () => [...present.values()].map(rowFor).sort((a, b) => a.name.localeCompare(b.name));
|
|
134
|
+
const toolsTool = defineTool('tools', {
|
|
135
|
+
search: {
|
|
136
|
+
type: 'string',
|
|
137
|
+
description: 'Filter by name or the one-line description. Omit to list every tool.',
|
|
138
|
+
},
|
|
139
|
+
describe: {
|
|
140
|
+
type: 'array',
|
|
141
|
+
items: { type: 'string' },
|
|
142
|
+
description: 'Tool names whose full schema you want before calling them.',
|
|
143
|
+
},
|
|
144
|
+
}, async (args) => {
|
|
145
|
+
const describe = Array.isArray(args.describe) ? args.describe.map(String) : [];
|
|
146
|
+
if (describe.length > 0) {
|
|
147
|
+
return describe.map((name) => {
|
|
148
|
+
const tool = present.get(name);
|
|
149
|
+
if (!tool) {
|
|
150
|
+
return { name, error: `No such tool: ${name}. Call tools for the full list of names.` };
|
|
151
|
+
}
|
|
152
|
+
return { ...rowFor(tool), schema: tool.schema.function };
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const q = typeof args.search === 'string' ? args.search.toLowerCase() : '';
|
|
156
|
+
return list().filter((row) => !q || row.name.includes(q) || row.line.toLowerCase().includes(q));
|
|
157
|
+
}, {
|
|
158
|
+
description: 'Every Ziggs tool that exists: name plus one line. A handful are already loaded by name; the rest are called through tool_read or tool_write. Pass describe with tool names to get their full schemas. Nothing is hidden.',
|
|
159
|
+
});
|
|
160
|
+
const dispatch = (via) => async (args, ctx) => {
|
|
161
|
+
const name = typeof args.tool === 'string' ? args.tool : '';
|
|
162
|
+
if (!name || !present.has(name)) {
|
|
163
|
+
throw new Error(`No such tool: ${name || '(missing)'}. Call tools for the full list of names.`);
|
|
164
|
+
}
|
|
165
|
+
refuseWrongLane(name, hostedToolLane(name), via);
|
|
166
|
+
const payload = args.args && typeof args.args === 'object' && !Array.isArray(args.args)
|
|
167
|
+
? args.args
|
|
168
|
+
: {};
|
|
169
|
+
return callCatalogTarget(name, payload, ctx, execute);
|
|
170
|
+
};
|
|
171
|
+
const toolRead = defineTool('tool_read', {
|
|
172
|
+
tool: { type: 'string', required: true, description: 'Read-only tool name from tools.' },
|
|
173
|
+
args: { type: 'object', description: 'Arguments for that tool, as its schema defines them.' },
|
|
174
|
+
}, dispatch('tool_read'), {
|
|
175
|
+
description: 'Call a read-only Ziggs tool by name. A tool that writes is refused here and pointed at tool_write. Look it up with tools describe=[name] first when you do not already know its parameters. The result is exactly what the tool itself returns.',
|
|
176
|
+
});
|
|
177
|
+
const toolWrite = defineTool('tool_write', {
|
|
178
|
+
tool: { type: 'string', required: true, description: 'Writing tool name from tools.' },
|
|
179
|
+
args: { type: 'object', description: 'Arguments for that tool, as its schema defines them.' },
|
|
180
|
+
}, dispatch('tool_write'), {
|
|
181
|
+
description: 'Call a Ziggs tool that creates or changes something, by name. Read-only tools are refused here and pointed at tool_read. Irreversible tools (agreement_revoke) are called by their own name only — this dispatcher refuses them. Look the tool up with tools describe=[name] first when you do not already know its parameters.',
|
|
182
|
+
});
|
|
183
|
+
return [toolsTool, toolRead, toolWrite];
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* The tool block the hosted model is shown.
|
|
187
|
+
*
|
|
188
|
+
* `execute` runs a catalog call through the ToolManager. Omit it when only
|
|
189
|
+
* the schemas are being priced — the dispatchers then refuse to run.
|
|
190
|
+
*/
|
|
191
|
+
export function hostedModelTools(all, execute) {
|
|
192
|
+
const present = new Map();
|
|
193
|
+
for (const tool of all) {
|
|
194
|
+
const name = toolName(tool);
|
|
195
|
+
if (!name || CATALOG.has(name))
|
|
196
|
+
continue;
|
|
197
|
+
present.set(name, tool);
|
|
198
|
+
}
|
|
199
|
+
const visible = [];
|
|
200
|
+
for (const [name, tool] of present) {
|
|
201
|
+
if (isHostedCatalogOnly(name))
|
|
202
|
+
continue;
|
|
203
|
+
visible.push(tool);
|
|
204
|
+
}
|
|
205
|
+
visible.push(...catalogTools(present, execute));
|
|
206
|
+
return visible;
|
|
207
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { RawEvent } from '../runtime/buildOutcome.js';
|
|
2
|
+
/**
|
|
3
|
+
* The wake gate: ask the backend whether chat messages need a
|
|
4
|
+
* model turn before the Claude brain starts one.
|
|
5
|
+
*
|
|
6
|
+
* The backend asks Jev (TypeSafe's System One model) one Choice — "asks this
|
|
7
|
+
* agent to act now" or "context to read later" — and answers `wake: false`
|
|
8
|
+
* only on a confident "context". The key lives there, not here: hosts run on
|
|
9
|
+
* customers' machines.
|
|
10
|
+
*/
|
|
11
|
+
export interface WakeGateMessage {
|
|
12
|
+
text: string;
|
|
13
|
+
senderId?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface WakeGateRequest {
|
|
16
|
+
messages: WakeGateMessage[];
|
|
17
|
+
agreementId?: string;
|
|
18
|
+
chatId?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface WakeGateAnswer {
|
|
21
|
+
wake: boolean;
|
|
22
|
+
/** `act` | `context` | `unsure` | `off`, as the backend decided. */
|
|
23
|
+
how?: string;
|
|
24
|
+
confidence?: number;
|
|
25
|
+
}
|
|
26
|
+
/** Null means "no answer": the caller wakes as it always has. */
|
|
27
|
+
export type AskWakeGate = (req: WakeGateRequest) => Promise<WakeGateAnswer | null>;
|
|
28
|
+
/**
|
|
29
|
+
* The messages this wake may ask about, or null when it may not ask at all.
|
|
30
|
+
*
|
|
31
|
+
* Only a wake made entirely of chat speech with text is asked about. Anything
|
|
32
|
+
* else in the batch — a task result, an agreement lifecycle event, a notice —
|
|
33
|
+
* wakes as today, decided here in code before Jev is involved: an assigned
|
|
34
|
+
* task, a control request (pause, stop) and a proposal naming this agent must
|
|
35
|
+
* never be skipped, and none of them is a `message`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function gateableMessages(event: RawEvent | undefined): WakeGateMessage[] | null;
|
|
38
|
+
/** The agreement the wake names, for the cost record. First one wins. */
|
|
39
|
+
export declare function wakeAgreementId(event: RawEvent | undefined): string | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* The default gate: `POST /internal/wake-gate` with the agent's operator key.
|
|
42
|
+
* Any failure — network, timeout, a refusal, a body without `wake` — is null,
|
|
43
|
+
* so a backend that predates the route costs one 404 and changes nothing.
|
|
44
|
+
*/
|
|
45
|
+
export declare function backendWakeGate(creds: {
|
|
46
|
+
operatorKey: string;
|
|
47
|
+
agentId: string;
|
|
48
|
+
}): AskWakeGate;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { buildOperatorHeaders, getBackendUrl } from '@ziggs-ai/api-client';
|
|
2
|
+
import { getBatchEvents } from '../context/batch.js';
|
|
3
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
4
|
+
/** Longer than Jev's own 4s budget on the server, shorter than a turn is worth waiting for. */
|
|
5
|
+
const TIMEOUT_MS = 6000;
|
|
6
|
+
/**
|
|
7
|
+
* The messages this wake may ask about, or null when it may not ask at all.
|
|
8
|
+
*
|
|
9
|
+
* Only a wake made entirely of chat speech with text is asked about. Anything
|
|
10
|
+
* else in the batch — a task result, an agreement lifecycle event, a notice —
|
|
11
|
+
* wakes as today, decided here in code before Jev is involved: an assigned
|
|
12
|
+
* task, a control request (pause, stop) and a proposal naming this agent must
|
|
13
|
+
* never be skipped, and none of them is a `message`.
|
|
14
|
+
*/
|
|
15
|
+
export function gateableMessages(event) {
|
|
16
|
+
const events = getBatchEvents(event);
|
|
17
|
+
if (events.length === 0)
|
|
18
|
+
return null;
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const ev of events) {
|
|
21
|
+
if (!ev || ev.type !== 'message')
|
|
22
|
+
return null;
|
|
23
|
+
const text = typeof ev.text === 'string' ? ev.text.trim() : '';
|
|
24
|
+
if (!text)
|
|
25
|
+
return null;
|
|
26
|
+
out.push({
|
|
27
|
+
text,
|
|
28
|
+
...(typeof ev.senderId === 'string' && ev.senderId ? { senderId: ev.senderId } : {}),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
/** The agreement the wake names, for the cost record. First one wins. */
|
|
34
|
+
export function wakeAgreementId(event) {
|
|
35
|
+
for (const ev of getBatchEvents(event)) {
|
|
36
|
+
const id = ev?.agreementId;
|
|
37
|
+
if (typeof id === 'string' && id)
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The default gate: `POST /internal/wake-gate` with the agent's operator key.
|
|
44
|
+
* Any failure — network, timeout, a refusal, a body without `wake` — is null,
|
|
45
|
+
* so a backend that predates the route costs one 404 and changes nothing.
|
|
46
|
+
*/
|
|
47
|
+
export function backendWakeGate(creds) {
|
|
48
|
+
return async (req) => {
|
|
49
|
+
if (!creds.operatorKey)
|
|
50
|
+
return null;
|
|
51
|
+
try {
|
|
52
|
+
const res = await fetch(`${getBackendUrl()}/internal/wake-gate`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
...buildOperatorHeaders(creds.operatorKey, creds.agentId),
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify(req),
|
|
59
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
await res.body?.cancel().catch(() => undefined);
|
|
63
|
+
runtimeLog.warn('wakeGate', `wake gate answered ${res.status} — waking as usual`);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const data = (await res.json().catch(() => null));
|
|
67
|
+
if (!data || typeof data.wake !== 'boolean')
|
|
68
|
+
return null;
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
runtimeLog.warn('wakeGate', `wake gate unreachable (${err?.name ?? 'error'}) — waking as usual`);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -69,6 +69,8 @@ export { PollRegistry, declaredPolls, type PollBinding, type PollContext, type P
|
|
|
69
69
|
export { PollRunner, type PollRunnerOptions, type WakeTarget } from './platform/polls/PollRunner.js';
|
|
70
70
|
export { PROTOCOL_TOOLS, agreementProposeTool, agreementSubcontractTool, agreementRespondTool, agreementClaimTool, agreementCounterTool, agreementGetTool, agreementRevokeTool, agreementFulfillTool, taskCreateTool, taskSetResultTool, taskUpdatePlanStepTool, dispatchProtocolOp, PAYMENT_TOOLS, paymentBalanceTool, CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, MCP_CONNECTION_TOOLS, mcpToolCallTool, mcpToolsListTool, DISCOVERY_TOOLS, agentSearchTool, agentGetTool, MARKETPLACE_TOOLS, marketplaceViewTool, CONTEXT_TOOLS, contextReadTool, contextRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, GRANTS_TOOLS, listGrantsTool, PLAN_TOOLS, taskReplacePlanTool, LINK_TOOLS, linkProposeTool, linkListTool, INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, ARTIFACT_TOOLS, recordArtifactTool, CHAT_TOOLS, openConversationTool, } from './platform/tools/index.js';
|
|
71
71
|
export { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
|
|
72
|
+
export { hostedModelTools, isHostedCatalogOnly, hostedToolLane } from './brain/hostedToolSurface.js';
|
|
73
|
+
export type { HostedToolExecute } from './brain/hostedToolSurface.js';
|
|
72
74
|
export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from './tasks/protocolRegistry.js';
|
|
73
75
|
export { InMemoryStore, FileMemoryStore } from './memory/MemoryStore.js';
|
|
74
76
|
export type { MemoryStore } from './memory/MemoryStore.js';
|
package/dist/index.js
CHANGED
|
@@ -83,6 +83,7 @@ PAYMENT_TOOLS, paymentBalanceTool, CONNECTION_TOOLS, connectionProxyTool, connec
|
|
|
83
83
|
// The always-on toolset for the hosted Claude cognition (parity guard
|
|
84
84
|
// reads this as part of the real SDK registry).
|
|
85
85
|
export { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
|
|
86
|
+
export { hostedModelTools, isHostedCatalogOnly, hostedToolLane } from './brain/hostedToolSurface.js';
|
|
86
87
|
export { PROTOCOL_TOOL_NAMES, PROTOCOL_TOOL_TO_OPERATION, mapProtocolToolToOperation, isProtocolToolName } from './tasks/protocolRegistry.js';
|
|
87
88
|
export { InMemoryStore, FileMemoryStore } from './memory/MemoryStore.js';
|
|
88
89
|
export { classifyIncomingEvent } from './context/classifyEnvelope.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@anthropic-ai/claude-agent-sdk": "^0.3.204",
|
|
27
27
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"@ziggs-ai/api-client": "0.
|
|
29
|
+
"@ziggs-ai/api-client": "0.24.0",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|