@ziggs-ai/agent-sdk 0.24.1 → 0.26.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 +40 -7
- 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 +5 -1
- package/dist/brain/claudeSdkTools.js +9 -46
- 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 +3 -1
- package/dist/index.js +2 -1
- package/dist/ingress/normalizeIncoming.d.ts +2 -0
- package/dist/ingress/normalizeIncoming.js +1 -0
- package/dist/platform/ZiggsEffectHandler.d.ts +1 -1
- package/dist/platform/ZiggsEffectHandler.js +1 -1
- package/dist/platform/inboxLoop.d.ts +3 -13
- package/dist/platform/inboxLoop.js +66 -115
- package/dist/platform/tools/index.d.ts +1 -1
- package/dist/platform/tools/index.js +1 -1
- package/dist/platform/tools/tier2/contextTools.d.ts +1 -0
- package/dist/platform/tools/tier2/contextTools.js +2 -1
- package/dist/runtime/PromptBuilder.js +2 -18
- package/dist/runtime/buildOutcome.d.ts +8 -0
- package/dist/runtime/buildOutcome.js +1 -14
- package/dist/types.d.ts +2 -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
|
};
|
|
@@ -308,7 +331,7 @@ export class Agent {
|
|
|
308
331
|
operatorKey: this.options.operatorKey,
|
|
309
332
|
agentId: this.options.agentId,
|
|
310
333
|
};
|
|
311
|
-
const messageSender = async (message, receiverId, sessionId, messageId, conversationAgreementId) => {
|
|
334
|
+
const messageSender = async (message, receiverId, sessionId, messageId, conversationAgreementId, underAgreementId, taskId) => {
|
|
312
335
|
const sentId = messageId ?? `sdk_${randomUUID()}`;
|
|
313
336
|
const result = await sendChatMessage({
|
|
314
337
|
chatId: sessionId,
|
|
@@ -322,6 +345,8 @@ export class Agent {
|
|
|
322
345
|
// only when absent (matches the old WS path letting the server assign).
|
|
323
346
|
messageId: sentId,
|
|
324
347
|
conversationAgreementId,
|
|
348
|
+
underAgreementId,
|
|
349
|
+
taskId,
|
|
325
350
|
}, creds);
|
|
326
351
|
if (!result.success)
|
|
327
352
|
throw new Error('sendChatMessage: the server did not accept this message — nothing was delivered and nobody was woken. Retry with the same messageId, which the backend dedups on.');
|
|
@@ -405,6 +430,14 @@ export class Agent {
|
|
|
405
430
|
maxTurns: this.options.claudeMaxTurns,
|
|
406
431
|
statelessPerWake: this.options.claudeStatelessPerWake,
|
|
407
432
|
actsOnHireActivation: this.options.actsOnHireActivation === true,
|
|
433
|
+
...(this.options.wakeGate === false || !this.options.operatorKey
|
|
434
|
+
? {}
|
|
435
|
+
: {
|
|
436
|
+
askWakeGate: backendWakeGate({
|
|
437
|
+
operatorKey: this.options.operatorKey,
|
|
438
|
+
agentId: this.options.agentId,
|
|
439
|
+
}),
|
|
440
|
+
}),
|
|
408
441
|
})
|
|
409
442
|
: new ZiggsBrain({
|
|
410
443
|
workflow,
|
|
@@ -1090,9 +1123,9 @@ export class Agent {
|
|
|
1090
1123
|
const sameRoom = 'sessionId' in effect && effect.sessionId === session.sessionId;
|
|
1091
1124
|
const scopedEffect = sameRoom && effect.kind === 'read-context'
|
|
1092
1125
|
? { ...effect, opts: { ...effect.opts, workScopes: contextScopesFromWake(frame, session.sessionId) } }
|
|
1093
|
-
:
|
|
1094
|
-
? { ...effect,
|
|
1095
|
-
:
|
|
1126
|
+
: agreementId && effect.kind === 'send-message'
|
|
1127
|
+
? { ...effect, underAgreementId: effect.underAgreementId ?? agreementId }
|
|
1128
|
+
: agreementId && (effect.kind === 'tool-call' || effect.kind === 'record-event')
|
|
1096
1129
|
? { ...effect, serviceAgreementId: effect.serviceAgreementId ?? agreementId }
|
|
1097
1130
|
: effect;
|
|
1098
1131
|
const operation = this.eff(scopedEffect);
|
|
@@ -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.
|
|
@@ -30,7 +34,7 @@ Your prose and final summary are private host notes, not messages to the people
|
|
|
30
34
|
You are headless — there is no human to prompt during a wake:
|
|
31
35
|
- When proposals or connection requests need a human decision, DO NOT approve or reject on their behalf. Summarize what needs the operator's attention and stop.
|
|
32
36
|
- When you ask somebody and stop, SAY SO ON THE TASK: \`task_update_plan_step\` with \`waitingOn\` ({ kind, id }), with the step you are ticking or on its own when the ask is all that happened. Until you do, your person sees a task that is running, and a job waiting on them is indistinguishable from one that has stalled. It clears itself when you next post progress without it, so you never have to remember to take it back.
|
|
33
|
-
- When the work needs something only the person who hired you can answer (a business detail, a decision, an access you were not given), ASK THEM: if you already have a conversation, \`chat_send\` with \`to\` set to their user id from \`context_snapshot\`'s \`users\`; first contact still needs \`chat_open\` then \`chat_send\` in the chat it returns —
|
|
37
|
+
- When the work needs something only the person who hired you can answer (a business detail, a decision, an access you were not given), ASK THEM: if you already have a conversation, \`chat_send\` with \`to\` set to their user id from \`context_snapshot\`'s \`users\`; first contact still needs \`chat_open\` then \`chat_send\` in the chat it returns — name that person as receiverId there; room membership never selects a recipient. Say what you need and why, then stop; their reply wakes you. Going quiet instead leaves the job parked with nobody asked. An id starting \`psn_\` is a masked face, not an address: that person belongs to the counterparty, and you reach them by messaging THEIR agent.
|
|
34
38
|
- When the work needs an answer from somebody you have no way to reach — a teammate of theirs, a partner, a customer, anyone \`chat_open\` refuses you — ASK THE PERSON YOU WORK FOR TO PUT YOU IN A ROOM WITH THEM. Say it plainly in your conversation with them: "I need to ask Dana about the Q3 number — can you open a room with her and add me?" Then stop; you are woken when you are in the room. Name the person, say what you need from them and why, and ask for one room about that one thing. Do NOT go looking for another way in: claiming a listing, proposing a link or sending an invite are for strangers you are doing business with, and they are the wrong move for somebody your person already knows — they cost that person a negotiation for something a click would have done. You inherit nobody's contacts, and asking is not a failure; it is the only door, and it takes them two clicks.
|
|
35
39
|
|
|
36
40
|
Security:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAgreementId, agreementLaneId
|
|
1
|
+
import { isAgreementId, agreementLaneId } from '@ziggs-ai/api-client';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { credsFromToolContext } from '../tools/toolCreds.js';
|
|
4
4
|
import { sendChatMessage, presentSendResult } from '@ziggs-ai/api-client';
|
|
@@ -67,14 +67,14 @@ 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
|
-
|
|
74
|
-
|
|
73
|
+
conversationAgreementId: { type: 'string', description: 'Explicit service work requested from the recipient, separate from your own run authority.' },
|
|
74
|
+
underAgreementId: { type: 'string', description: 'Your run authority, defaulting to the current run in every destination room.' },
|
|
75
75
|
chatId: { type: 'string', description: 'Omit this to send in the chat you were woken in — that is the usual case. Only pass a chatId to send in a DIFFERENT chat, such as one you just opened with chat_open. When chatId is set it wins over `to`.' },
|
|
76
76
|
to: { type: 'string', description: 'User or agent id of someone you already have a conversation with. Sends in that pair room. Does not open contact — first contact still needs chat_open. Ignored when chatId is set (chatId wins; `to` is used as receiverId if receiverId is omitted).' },
|
|
77
|
-
receiverId: { type: 'string', description: '
|
|
77
|
+
receiverId: { type: 'string', description: 'Explicit recipient user or agent id, or a participant reference from this destination room. Omit to let Jev choose when confident; otherwise this is room context with no specific wake. The wake sender and last writer are never defaults. Pass human for an intentional broadcast to people.' },
|
|
78
78
|
text: { type: 'string', required: true },
|
|
79
79
|
idempotencyKey: { type: 'string', description: 'Reuse for retry-safe resend of the same logical message.' },
|
|
80
80
|
}, async (args, ctx) => {
|
|
@@ -95,8 +95,8 @@ export const chatSendTool = defineTool('chat_send', {
|
|
|
95
95
|
text,
|
|
96
96
|
messageId,
|
|
97
97
|
conversationAgreementId: firstFilled(args.conversationAgreementId),
|
|
98
|
+
underAgreementId: firstFilled(args.underAgreementId, c.serviceAgreementId),
|
|
98
99
|
taskId: firstFilled(args.taskId),
|
|
99
|
-
replyToMessageId: firstFilled(args.replyToMessageId),
|
|
100
100
|
...(namedReceiver ? { receiverId: namedReceiver } : {}),
|
|
101
101
|
}, creds);
|
|
102
102
|
// Never presented as a send that happened: the message does not exist,
|
|
@@ -112,48 +112,11 @@ export const chatSendTool = defineTool('chat_send', {
|
|
|
112
112
|
throw new Error('chat_send requires eff in tool context');
|
|
113
113
|
// Same agreement-id-where-a-chat-id-goes slip as context_snapshot, and the
|
|
114
114
|
// same mapping: an agreement's traffic lane is `agrn-<agreementId>`.
|
|
115
|
-
const wakeChatId = asChatLane(firstFilled(c.chatId, c.sessionId));
|
|
116
115
|
const sessionId = asChatLane(firstFilled(args.chatId, c.chatId, c.sessionId));
|
|
117
116
|
if (!sessionId) {
|
|
118
117
|
throw new Error('chat_send: this wake carries no chat to send in. Pass chatId of the room, or to naming a person you already have a conversation with (first contact still needs chat_open).');
|
|
119
118
|
}
|
|
120
|
-
|
|
121
|
-
// the model spell it out cost a failed tool call + a full extra LLM turn
|
|
122
|
-
// per reply.
|
|
123
|
-
//
|
|
124
|
-
// But ONLY in the room that wake belongs to. The sender an
|
|
125
|
-
// event carries is usually an `rpb_*` room participant reference, which is
|
|
126
|
-
// an address inside its own room and a dead id in any other. Defaulting it
|
|
127
|
-
// into a different chat is how an agent that opened a chat with its own
|
|
128
|
-
// human — `chat_open` accepted the principal and minted the room — had the
|
|
129
|
-
// very next `chat_send` refused with INVALID_ROOM_PARTICIPANT_REF, and gave
|
|
130
|
-
// up on reaching the person who hired it. An explicitly passed foreign-room
|
|
131
|
-
// ref is dropped only when it is the wake sender carried across rooms.
|
|
132
|
-
// A DIFFERENT explicit ref can come from the destination's snapshot and
|
|
133
|
-
// must reach the server, which validates it in that destination room.
|
|
134
|
-
const sameRoom = sessionId === wakeChatId;
|
|
135
|
-
const explicit = firstFilled(args.receiverId, args.to);
|
|
136
|
-
const receiverId = explicit && !(isRoomParticipantRef(explicit) && !sameRoom && explicit === c.senderId)
|
|
137
|
-
? explicit
|
|
138
|
-
: sameRoom
|
|
139
|
-
? firstFilled(c.senderId)
|
|
140
|
-
: undefined;
|
|
141
|
-
if (!sameRoom && !receiverId) {
|
|
142
|
-
// A room that also holds the person's old delegate is no longer 1:1.
|
|
143
|
-
// An omitted recipient there is refused, not a broadcast; name the
|
|
144
|
-
// person or agent, or pass human for a room broadcast.
|
|
145
|
-
const snapshot = await c.eff({
|
|
146
|
-
kind: 'read-context', sessionId, opts: { maxMessages: 1 },
|
|
147
|
-
});
|
|
148
|
-
const participantCount = (snapshot?.users?.length ?? 0) + (snapshot?.agents?.length ?? 0);
|
|
149
|
-
if (participantCount > 2) {
|
|
150
|
-
throw new Error('chat_send: this destination has several participants. Set receiverId to the person or agent you are asking, using their account id or a participant reference from context_snapshot of this destination. Omit no recipient when expecting a reply. For an intentional human broadcast, explicitly set receiverId="human". No message was sent.');
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
// No receiver is a legitimate send, not a failure: the backend resolves the
|
|
154
|
-
// recipient from the room's own participants and answers with a 1:1 chat's
|
|
155
|
-
// only other member. That is exactly the case a freshly opened chat is in,
|
|
156
|
-
// and it is the one case the wake's sender cannot answer.
|
|
119
|
+
const receiverId = firstFilled(args.receiverId, args.to);
|
|
157
120
|
// Default to the wake's content-derived key: a crash-replay that
|
|
158
121
|
// re-sends the same text re-derives the same messageId and the backend drops
|
|
159
122
|
// the duplicate; a re-think with different text gets a new key
|
|
@@ -166,8 +129,8 @@ export const chatSendTool = defineTool('chat_send', {
|
|
|
166
129
|
return c.eff({
|
|
167
130
|
kind: 'send-message',
|
|
168
131
|
...(firstFilled(args.taskId) ? { taskId: firstFilled(args.taskId) } : {}),
|
|
169
|
-
...(firstFilled(args.
|
|
170
|
-
...(firstFilled(args.
|
|
132
|
+
...(firstFilled(args.conversationAgreementId) ? { conversationAgreementId: firstFilled(args.conversationAgreementId) } : {}),
|
|
133
|
+
...(firstFilled(args.underAgreementId, c.serviceAgreementId) ? { underAgreementId: firstFilled(args.underAgreementId, c.serviceAgreementId) } : {}),
|
|
171
134
|
sessionId,
|
|
172
135
|
...(receiverId ? { receiverId } : {}),
|
|
173
136
|
text,
|
|
@@ -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 {};
|