@ziggs-ai/agent-sdk 0.28.0 → 0.30.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/dist/Agent.js +3 -6
- package/dist/brain/ClaudeBrain.d.ts +4 -5
- package/dist/brain/ClaudeBrain.js +26 -10
- package/dist/brain/claudeCognition.d.ts +2 -2
- package/dist/brain/claudeCognition.js +6 -5
- package/dist/brain/claudeSdkProtocol.js +5 -5
- package/dist/brain/claudeSdkTools.js +8 -2
- package/dist/memory/MemoryStore.d.ts +12 -9
- package/dist/memory/MemoryStore.js +21 -7
- package/dist/platform/ZiggsEffectHandler.js +1 -1
- package/dist/runtime/agreementContext.d.ts +9 -1
- package/dist/runtime/agreementContext.js +24 -4
- package/dist/runtime/buildOutcome.d.ts +0 -9
- package/dist/runtime/buildOutcome.js +0 -24
- package/dist/runtime/kickoff.js +5 -5
- package/dist/runtime/wakeEffects.js +9 -1
- package/dist/types.d.ts +2 -0
- package/package.json +3 -3
package/dist/Agent.js
CHANGED
|
@@ -650,18 +650,15 @@ export class Agent {
|
|
|
650
650
|
}
|
|
651
651
|
memoryForWake(session, event) {
|
|
652
652
|
const fromEvent = hireIdsFromEvent(event);
|
|
653
|
-
const wakeAgreementIds = uniqueHireIds([
|
|
654
|
-
...fromEvent,
|
|
655
|
-
session.actingHireId,
|
|
656
|
-
]);
|
|
653
|
+
const wakeAgreementIds = fromEvent.length ? fromEvent : uniqueHireIds([session.actingHireId]);
|
|
657
654
|
const section = memorySectionForWake({
|
|
658
655
|
sessionId: session.sessionId,
|
|
659
|
-
activeAgreementId: session.ctx
|
|
656
|
+
activeAgreementId: fromEvent.length ? undefined : session.ctx
|
|
660
657
|
?.activeAgreementId,
|
|
661
658
|
wakeAgreementIds,
|
|
662
659
|
});
|
|
663
660
|
const known = uniqueHireIds([
|
|
664
|
-
session.ctx
|
|
661
|
+
fromEvent.length ? undefined : session.ctx
|
|
665
662
|
?.activeAgreementId,
|
|
666
663
|
...wakeAgreementIds,
|
|
667
664
|
]);
|
|
@@ -19,12 +19,11 @@ export { isPublishProbeChat, isCapabilityProbeSession } from '../probeChat.js';
|
|
|
19
19
|
*
|
|
20
20
|
* Empty / missing batch → notice-only (do not burn a turn on a malformed wake).
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* message
|
|
25
|
-
* real wake — skipped here is the LLM turn, not the information.
|
|
22
|
+
* Who a message is for is not decided here: the backend picks the receiver,
|
|
23
|
+
* and the inbox hands this agent only the rows assigned to it. Whether a
|
|
24
|
+
* message needs a turn at all is the wake gate's question.
|
|
26
25
|
*/
|
|
27
|
-
export declare function isNoticeOnlyWake(event: RawEvent | undefined
|
|
26
|
+
export declare function isNoticeOnlyWake(event: RawEvent | undefined): boolean;
|
|
28
27
|
export type CognitionKind = 'fsm' | 'claude-sdk';
|
|
29
28
|
export interface ClaudeBrainOptions {
|
|
30
29
|
workflow: Workflow;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { getAgreement } from '@ziggs-ai/api-client';
|
|
2
2
|
import { permitsAutomaticMandate } from '../runtime/agreementContext.js';
|
|
3
3
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
|
4
|
-
import {
|
|
4
|
+
import { outcomeFromEvent } from '../runtime/buildOutcome.js';
|
|
5
5
|
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 { hireIdsFromEvent, memorySectionForWake } from '../memory/MemoryStore.js';
|
|
9
10
|
import { hostedModelTools } from './hostedToolSurface.js';
|
|
10
11
|
import { buildZiggsSdkMcpServer } from './toolManagerToSdkMcp.js';
|
|
11
12
|
import { claudeQueryOptions, buildClaudeEventPrompt, summarizeContext, extractAssistantText, } from './claudeCognition.js';
|
|
@@ -30,17 +31,15 @@ export { isPublishProbeChat, isCapabilityProbeSession } from '../probeChat.js';
|
|
|
30
31
|
*
|
|
31
32
|
* Empty / missing batch → notice-only (do not burn a turn on a malformed wake).
|
|
32
33
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* message
|
|
36
|
-
* real wake — skipped here is the LLM turn, not the information.
|
|
34
|
+
* Who a message is for is not decided here: the backend picks the receiver,
|
|
35
|
+
* and the inbox hands this agent only the rows assigned to it. Whether a
|
|
36
|
+
* message needs a turn at all is the wake gate's question.
|
|
37
37
|
*/
|
|
38
|
-
export function isNoticeOnlyWake(event
|
|
38
|
+
export function isNoticeOnlyWake(event) {
|
|
39
39
|
const events = getBatchEvents(event);
|
|
40
40
|
if (events.length === 0)
|
|
41
41
|
return true;
|
|
42
|
-
return events.every((ev) => ev?.type === 'notification'
|
|
43
|
-
isMessageAddressedElsewhere(ev, ownAgentId));
|
|
42
|
+
return events.every((ev) => ev?.type === 'notification');
|
|
44
43
|
}
|
|
45
44
|
const DEFAULT_STATE = 'ready';
|
|
46
45
|
/**
|
|
@@ -264,7 +263,7 @@ export class ClaudeBrain {
|
|
|
264
263
|
skippedReason: 'agent_formed_hire_activation',
|
|
265
264
|
};
|
|
266
265
|
}
|
|
267
|
-
if (isNoticeOnlyWake(rawEvent
|
|
266
|
+
if (isNoticeOnlyWake(rawEvent)) {
|
|
268
267
|
runtimeLog.info('ClaudeBrain', `notice-only wake — skipping LLM session=${identity.sessionId} agent=${this.agentId} ` +
|
|
269
268
|
`type=${rawEvent?.type ?? 'unknown'}`);
|
|
270
269
|
return {
|
|
@@ -281,7 +280,7 @@ export class ClaudeBrain {
|
|
|
281
280
|
nextCtx.lastOutcome = outcomeFromEvent(rawEvent ?? null, identity.agentId);
|
|
282
281
|
runtimeLog.info('ClaudeBrain', `standing listing acceptance — no customer hire agent=${this.agentId} ` +
|
|
283
282
|
`agreements=${listingCheck.ignoredAgreementIds.join(',')}`);
|
|
284
|
-
if (!rawEvent || isNoticeOnlyWake(rawEvent
|
|
283
|
+
if (!rawEvent || isNoticeOnlyWake(rawEvent)) {
|
|
285
284
|
return {
|
|
286
285
|
ctx: nextCtx,
|
|
287
286
|
state: state ?? DEFAULT_STATE,
|
|
@@ -447,6 +446,23 @@ export class ClaudeBrain {
|
|
|
447
446
|
runtimeLog.warn('ClaudeBrain', `stderr session=${identity.sessionId}: ${line}`);
|
|
448
447
|
},
|
|
449
448
|
});
|
|
449
|
+
const workIds = hireIdsFromEvent(rawEvent);
|
|
450
|
+
const selectedWork = contextSnap?.work;
|
|
451
|
+
const workScope = JSON.stringify([
|
|
452
|
+
selectedWork?.contexts?.map(p => [p.agreementId, p.task?.taskId]) ?? [selectedWork?.agreementId, selectedWork?.task?.taskId],
|
|
453
|
+
memorySectionForWake({
|
|
454
|
+
sessionId: identity.sessionId,
|
|
455
|
+
wakeAgreementIds: workIds,
|
|
456
|
+
activeAgreementId: workIds.length ? undefined : nextCtx.activeAgreementId,
|
|
457
|
+
}),
|
|
458
|
+
contextSnap?.workBrief?.audience ??
|
|
459
|
+
contextSnap?.work?.contexts?.map(p => p.workBrief?.audience ?? null) ?? {
|
|
460
|
+
users: contextSnap?.users ?? [], agents: contextSnap?.agents ?? [],
|
|
461
|
+
},
|
|
462
|
+
]);
|
|
463
|
+
if (nextCtx.claudeWorkScope !== workScope)
|
|
464
|
+
nextCtx.claudeSessionId = undefined;
|
|
465
|
+
nextCtx.claudeWorkScope = workScope;
|
|
450
466
|
let metrics = { llmCalls: 0, tokens: { total: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 } };
|
|
451
467
|
let sessionId = nextCtx.claudeSessionId;
|
|
452
468
|
// Cost-weighted, cumulative: what this wake has charged the key so far.
|
|
@@ -66,8 +66,8 @@ export declare function resolveClaudeModel(preferred?: string, servicesModel?: s
|
|
|
66
66
|
* Caching is a prefix match, so block ORDER decides how far the hit reaches:
|
|
67
67
|
* the protocol block is byte-identical for every hosted agent and goes first,
|
|
68
68
|
* the role block is per-agent and goes second. An agent whose combined prompt
|
|
69
|
-
* lands under the model's minimum cacheable prefix (
|
|
70
|
-
* on Haiku) simply writes no entry — that is a silent no-op, not an error, so
|
|
69
|
+
* lands under the model's minimum cacheable prefix (512 tokens on Sonnet 5.5,
|
|
70
|
+
* 4,096 on Haiku 4.5) simply writes no entry — that is a silent no-op, not an error, so
|
|
71
71
|
* read `cacheRead`/`cacheCreation` in the wake metrics rather than assuming.
|
|
72
72
|
*/
|
|
73
73
|
export declare function claudeQueryOptions(params: ClaudeQueryOptionsParams): Options;
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { SYSTEM_PROMPT_DYNAMIC_BOUNDARY, } from '@anthropic-ai/claude-agent-sdk';
|
|
15
15
|
import { decisionWords } from '@ziggs-ai/api-client';
|
|
16
16
|
import { getBatchEvents } from '../context/batch.js';
|
|
17
|
-
const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-
|
|
17
|
+
const DEFAULT_CLAUDE_MODEL = 'claude-sonnet-5-5';
|
|
18
18
|
/**
|
|
19
19
|
* Pick the Claude model for a Claude-brain wake. The brain must run a `claude-*`
|
|
20
20
|
* model, but the host's default model is provider-agnostic (an OpenAI id today),
|
|
@@ -47,8 +47,8 @@ export function resolveClaudeModel(preferred, servicesModel) {
|
|
|
47
47
|
* Caching is a prefix match, so block ORDER decides how far the hit reaches:
|
|
48
48
|
* the protocol block is byte-identical for every hosted agent and goes first,
|
|
49
49
|
* the role block is per-agent and goes second. An agent whose combined prompt
|
|
50
|
-
* lands under the model's minimum cacheable prefix (
|
|
51
|
-
* on Haiku) simply writes no entry — that is a silent no-op, not an error, so
|
|
50
|
+
* lands under the model's minimum cacheable prefix (512 tokens on Sonnet 5.5,
|
|
51
|
+
* 4,096 on Haiku 4.5) simply writes no entry — that is a silent no-op, not an error, so
|
|
52
52
|
* read `cacheRead`/`cacheCreation` in the wake metrics rather than assuming.
|
|
53
53
|
*/
|
|
54
54
|
export function claudeQueryOptions(params) {
|
|
@@ -99,7 +99,7 @@ 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}${directArtifactGrantLine(snap)}${snap.work ? `\nWork context (exact scope, governing constraints and required dependencies):\n${JSON.stringify(snap.work)}` : ''}`;
|
|
102
|
+
return `${snap.workBrief ? `Work brief:\n${JSON.stringify(snap.workBrief)}\n` : ''}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
103
|
}
|
|
104
104
|
/** Ids and names only. Absent when the wake has nothing outside the room inventory. */
|
|
105
105
|
function directArtifactGrantLine(snap) {
|
|
@@ -277,7 +277,8 @@ function renderEventLines(event, lines, now) {
|
|
|
277
277
|
'';
|
|
278
278
|
lines.push('Work signal: listing claimed (proposal_approved). Verify the accepted interaction mode. For task-only agreements, wait for assigned tasks and report results through tasks; no chat or greeting. Only if messages are accepted, call chat_open with the buyer' +
|
|
279
279
|
(buyerId ? ` (participantId=${buyerId})` : ' (actorId / Sender on this event)') +
|
|
280
|
-
|
|
280
|
+
(agreementId ? ` and agreementId=${agreementId}` : ' and the claimed agreementId') +
|
|
281
|
+
". That returns the hire's room, which the server usually made at claim, or opens one when there is none; then greet them and serve the agreement terms. Do not chat_send to an agrn-* session id — open a real chat first.");
|
|
281
282
|
if (buyerId)
|
|
282
283
|
lines.push(`Buyer (claim actor): ${buyerId}`);
|
|
283
284
|
// Prefer nested resource agreementId when the outer event omitted it.
|
|
@@ -14,7 +14,7 @@ import { INBOUND_DISCOVERY_POLICY } from '../inboundDiscoveryPolicy.js';
|
|
|
14
14
|
const loadedByName = EVERYDAY_SDK_TOOLS.map((name) => `\`${name}\``).join(', ');
|
|
15
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.
|
|
16
16
|
|
|
17
|
-
Each wake hands you ONE event (or a small batch). Handle
|
|
17
|
+
Each wake hands you ONE event (or a small batch). Read workBrief for current work and audience. Resource open/access_explain returns the same scoped actions: use tool, args and required inputs; recovery names a suggested next call. Available is checked at read time, needs_decision names missing authority, unavailable names a known blocker, and not_evaluated requires further inputs or execution checks. It is backend-resolved context, not a permission token. Use normal tools for actions marked not_evaluated; that state alone does not require human approval. Never infer authority from role names or combine customers’ permissions. actingFor.state ambiguous means several customers are present and only your own account authority applies; relationship.side is the agreement side you hold and authorizes nothing by itself. A chat_send refused with WORK_SELECTION_REQUIRED lists the eligible agreements in choices: set conversationAgreementId to one of them and send again; never guess which one. Handle the event, then stop.
|
|
18
18
|
|
|
19
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
20
|
|
|
@@ -27,19 +27,19 @@ Your prose and final summary are private host notes, not messages to the people
|
|
|
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).
|
|
29
29
|
- Chat is where you talk to whoever you are working with, and where a human reads what you delivered — sending it to them there is delivery, not duplication. It is not where another AGENT collects a deliverable: that one reads the task result, so a chat message never stands in for closing the task. Deliver into the working conversation the hire already has; do not repeat it into every room you can reach.
|
|
30
|
-
- When a store listing of yours is claimed (\`resource_changed\` with reason \`proposal_approved
|
|
30
|
+
- When a store listing of yours is claimed (\`resource_changed\` with reason \`proposal_approved\`), call \`chat_open\` with the buyer (actorId / Sender) AND the claimed agreementId. That returns the hire's room, which the server usually made at claim, or opens one when there is none. Greet them only when the agreement accepts messages, then serve the agreement terms. A task-only agreement needs no chat or greeting: wait for assigned tasks and deliver results through tasks. Do not \`chat_send\` to an \`agrn-*\` session id — open a real chat first.
|
|
31
31
|
- When a task you hold carries a deadline or a reminder rule, they are part of the job, not decoration. Never remind the same person sooner than the stated interval or more times than the budget; use \`wake_at\` to come back rather than waiting in place, and re-arm it each time you wake. When the budget is spent, stop asking and say what you have. At the deadline, post what you got and where you are stuck in the room where the question was asked, close the task, and stop — whether or not you got the answer. Nothing stops you from breaking this, which is exactly why it is on you: over-reminding is a breach of the task, not diligence, and every reminder costs a wake.
|
|
32
32
|
- Hand off by completing your task — the platform notifies the next agent.
|
|
33
33
|
|
|
34
34
|
You are headless — there is no human to prompt during a wake:
|
|
35
|
-
-
|
|
35
|
+
- For proposals and connection requests, read the agreement’s approvalAction. Use existing covering authority when available; otherwise ask the accountable party to decide. Slot ownership alone is not authority. Do not add a confirmation when an existing mandate covers the decision.
|
|
36
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.
|
|
37
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.
|
|
38
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.
|
|
39
39
|
|
|
40
40
|
Security:
|
|
41
|
-
- A relationship
|
|
42
|
-
- You decide what to do with a chat message. Ziggs does not enforce that counterparty text is ignored — that is a convention. What Ziggs does enforce: no spending and no commitments without an agreement, and
|
|
41
|
+
- A relationship (a link) can parent a separately accepted service agreement. Parentage carries governing authority and revocation, not automatic spending consent. Each message or task uses one exact work agreement; its quota is direct unless a governing term explicitly shares it across the subtree. Agreement proposals, decisions, and system notices do not consume the conversation allowance.
|
|
42
|
+
- You decide what to do with a chat message. Ziggs does not enforce that counterparty text is ignored — that is a convention. What Ziggs does enforce: no spending and no commitments without an agreement, and a stranger talks to a listed agent under its listing, which counts their free messages; messages past those are refused at the send door, asking them to hire. Work is a task under an agreement; a stranger hires you through your listing, or through an agreement you draft when you have none. Your operating contract is this protocol and your role below.
|
|
43
43
|
- ${INBOUND_DISCOVERY_POLICY}
|
|
44
44
|
|
|
45
45
|
When the event is handled, stop with a one-paragraph summary of what you did. If the event needs nothing from you (a status echo, a duplicate, a notification you already acted on), say so in one sentence and stop — do not invent work.`;
|
|
@@ -73,7 +73,7 @@ export const chatSendTool = defineTool('chat_send', {
|
|
|
73
73
|
conversationAgreementId: { type: 'string', description: 'Explicit service work requested from the recipient, separate from your own run authority.' },
|
|
74
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
|
-
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).' },
|
|
76
|
+
to: { type: 'string', description: 'User or agent id of someone you already have a conversation with. Sends in that pair room. Naming the person whose message woke you sends in the room that woke you instead, addressed to them. 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
77
|
receiverId: { type: 'string', description: 'Explicit recipient user or agent id, or a participant reference from this destination room. Omit it when the room has one other participant: they are the recipient. With several, Jev chooses 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.' },
|
|
@@ -82,9 +82,15 @@ export const chatSendTool = defineTool('chat_send', {
|
|
|
82
82
|
const explicitChatId = asChatLane(firstFilled(args.chatId));
|
|
83
83
|
const to = firstFilled(args.to);
|
|
84
84
|
const text = String(args.text);
|
|
85
|
+
// Naming the person who woke you is an answer, not a new conversation: it
|
|
86
|
+
// goes in the room that woke you, addressed to them. A probe room or a
|
|
87
|
+
// stranger's room has no pair room with that person, and asking the server
|
|
88
|
+
// for one only bought a refusal and a chat_open round trip.
|
|
89
|
+
const wakeChat = asChatLane(firstFilled(c.chatId, c.sessionId));
|
|
90
|
+
const answersWakeSender = !!to && !!wakeChat && to === c.senderId;
|
|
85
91
|
// Existing conversation with a person: resolve their pair room server-side.
|
|
86
92
|
// Does not open contact. An explicit chatId still wins.
|
|
87
|
-
if (!explicitChatId && to) {
|
|
93
|
+
if (!explicitChatId && to && !answersWakeSender) {
|
|
88
94
|
const creds = credsFromToolContext(c, 'chat_send');
|
|
89
95
|
const messageId = args.idempotencyKey
|
|
90
96
|
? `sdk_idem_${String(args.idempotencyKey)}`
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This is the SDK's contract; the *storage* is operator-provided. The agent
|
|
5
5
|
* picks the key shape (often `counterparty:<id>`, sometimes `agreement:<id>:notes`,
|
|
6
|
-
* etc.) — the SDK does not enforce one.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* etc.) — the SDK does not enforce one. Notes are bound to an agreement
|
|
7
|
+
* (Ido, 2026-09-29): the host prefixes every key with the one agreement the
|
|
8
|
+
* wake is under, and a wake under none or several has no notes at all. A
|
|
9
|
+
* room is not a drawer, and neither is the agent as a whole.
|
|
9
10
|
*
|
|
10
11
|
* Reference implementations: `InMemoryStore` (default; non-durable) and
|
|
11
12
|
* `FileMemoryStore` (single JSON file; useful for local dev). Anything
|
|
@@ -60,17 +61,19 @@ export declare class FileMemoryStore implements MemoryStore {
|
|
|
60
61
|
*
|
|
61
62
|
* Pick from the hire this wake is acting under — the same fact the
|
|
62
63
|
* server uses for acting-for: a pinned agreement, the delivery / event
|
|
63
|
-
* hire, or an `agrn-` lane.
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
64
|
+
* hire, or an `agrn-` lane. Exactly one agreement gives that agreement's
|
|
65
|
+
* section. None, or several, gives null: notes are bound to an agreement,
|
|
66
|
+
* so such a wake has nowhere to keep them, and says so on a write rather
|
|
67
|
+
* than landing in a room's or the agent's shared drawer.
|
|
67
68
|
*/
|
|
68
69
|
export declare function memorySectionForWake(input: {
|
|
69
70
|
sessionId?: string | null;
|
|
70
71
|
activeAgreementId?: string | null;
|
|
71
72
|
/** Hire ids already on this wake (delivery / event / acting-for). */
|
|
72
73
|
wakeAgreementIds?: readonly (string | null | undefined)[];
|
|
73
|
-
}): string;
|
|
74
|
+
}): string | null;
|
|
75
|
+
/** The notes of a wake that has no single agreement: nothing to read, and a write says why. */
|
|
76
|
+
export declare function noAgreementMemoryStore(): MemoryStore;
|
|
74
77
|
/** Distinct non-empty hire ids, first-seen order. */
|
|
75
78
|
export declare function uniqueHireIds(raw: readonly (string | null | undefined)[]): string[];
|
|
76
79
|
/**
|
|
@@ -83,4 +86,4 @@ export declare function hireIdsFromEvent(event: unknown): string[];
|
|
|
83
86
|
* Present a store as if it only contained one section. Agent keys stay
|
|
84
87
|
* unchanged; get/put/list/delete all go through `section:`.
|
|
85
88
|
*/
|
|
86
|
-
export declare function partitionMemoryStore(store: MemoryStore, section: string): MemoryStore;
|
|
89
|
+
export declare function partitionMemoryStore(store: MemoryStore, section: string | null): MemoryStore;
|
|
@@ -82,10 +82,10 @@ export class FileMemoryStore {
|
|
|
82
82
|
*
|
|
83
83
|
* Pick from the hire this wake is acting under — the same fact the
|
|
84
84
|
* server uses for acting-for: a pinned agreement, the delivery / event
|
|
85
|
-
* hire, or an `agrn-` lane.
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
85
|
+
* hire, or an `agrn-` lane. Exactly one agreement gives that agreement's
|
|
86
|
+
* section. None, or several, gives null: notes are bound to an agreement,
|
|
87
|
+
* so such a wake has nowhere to keep them, and says so on a write rather
|
|
88
|
+
* than landing in a room's or the agent's shared drawer.
|
|
89
89
|
*/
|
|
90
90
|
export function memorySectionForWake(input) {
|
|
91
91
|
const ids = uniqueHireIds([
|
|
@@ -93,9 +93,21 @@ export function memorySectionForWake(input) {
|
|
|
93
93
|
...(input.wakeAgreementIds ?? []),
|
|
94
94
|
agreementIdFromAgrnSession(input.sessionId),
|
|
95
95
|
]);
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
return ids.length === 1 ? `hire:${ids[0]}` : null;
|
|
97
|
+
}
|
|
98
|
+
const NO_AGREEMENT_NOTES = 'Notes are kept per agreement, and this wake is under none or several, so there is nothing to keep them under. Name the work this note belongs to, or keep it in the reply.';
|
|
99
|
+
/** The notes of a wake that has no single agreement: nothing to read, and a write says why. */
|
|
100
|
+
export function noAgreementMemoryStore() {
|
|
101
|
+
return {
|
|
102
|
+
get: async () => undefined,
|
|
103
|
+
put: async () => {
|
|
104
|
+
throw new Error(NO_AGREEMENT_NOTES);
|
|
105
|
+
},
|
|
106
|
+
list: async () => [],
|
|
107
|
+
delete: async () => {
|
|
108
|
+
throw new Error(NO_AGREEMENT_NOTES);
|
|
109
|
+
},
|
|
110
|
+
};
|
|
99
111
|
}
|
|
100
112
|
/** Distinct non-empty hire ids, first-seen order. */
|
|
101
113
|
export function uniqueHireIds(raw) {
|
|
@@ -142,6 +154,8 @@ function agreementIdFromAgrnSession(sessionId) {
|
|
|
142
154
|
* unchanged; get/put/list/delete all go through `section:`.
|
|
143
155
|
*/
|
|
144
156
|
export function partitionMemoryStore(store, section) {
|
|
157
|
+
if (section === null)
|
|
158
|
+
return noAgreementMemoryStore();
|
|
145
159
|
const prefix = `${section}:`;
|
|
146
160
|
const partitioned = {
|
|
147
161
|
get: (key) => store.get(prefix + key),
|
|
@@ -245,7 +245,7 @@ export function createZiggsEffectHandler(deps) {
|
|
|
245
245
|
const packets = await Promise.all((scopes.length ? scopes : [undefined]).map(workScope => buildSnapshot(deps, effect.sessionId, deps.agentId, { limit: effect.opts?.maxMessages, workScope })));
|
|
246
246
|
const snap = packets.length === 1 ? packets[0] : {
|
|
247
247
|
...emptySnapshot(deps.agentId),
|
|
248
|
-
work: { contexts: packets.map(p => ({ ...p.work, agreements: p.agreements, history: p.history, artifacts: p.artifacts })),
|
|
248
|
+
work: { contexts: packets.map(p => ({ ...p.work, workBrief: p.workBrief, agreements: p.agreements, history: p.history, artifacts: p.artifacts })),
|
|
249
249
|
instruction: 'Separate work items: select one exact agreement for each action. Context and authority do not transfer between these packets.' },
|
|
250
250
|
unavailable: packets.some(p => p.unavailable),
|
|
251
251
|
};
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A seller's standing offer: open to everyone (`everyone`) or one
|
|
3
|
+
* company (`org`). A listing is never work: claiming it gives each claimer
|
|
4
|
+
* their own hire, so nothing is done or spent under the listing itself. Reads
|
|
5
|
+
* both party shapes: an agreement row (`{ principal }`) and a room snapshot
|
|
6
|
+
* row (the principal as a string).
|
|
7
|
+
*/
|
|
8
|
+
export declare function isListing(agreement: unknown): boolean;
|
|
1
9
|
/** Enrich each wake from the agreement it actually names, never from the room's
|
|
2
10
|
* latest deal. Reads use the agent's credentials; no grants are inferred here. */
|
|
3
11
|
export declare function withAgreementContext(event: unknown, read: (agreementId: string) => Promise<unknown>): Promise<unknown>;
|
|
4
|
-
/**
|
|
12
|
+
/** A link or a listing supplies conversation context, never consent to another engagement. */
|
|
5
13
|
export declare function permitsAutomaticMandate(event: unknown): boolean;
|
|
@@ -1,4 +1,17 @@
|
|
|
1
1
|
const row = (value) => value && typeof value === 'object' ? value : {};
|
|
2
|
+
/**
|
|
3
|
+
* A seller's standing offer: open to everyone (`everyone`) or one
|
|
4
|
+
* company (`org`). A listing is never work: claiming it gives each claimer
|
|
5
|
+
* their own hire, so nothing is done or spent under the listing itself. Reads
|
|
6
|
+
* both party shapes: an agreement row (`{ principal }`) and a room snapshot
|
|
7
|
+
* row (the principal as a string).
|
|
8
|
+
*/
|
|
9
|
+
export function isListing(agreement) {
|
|
10
|
+
const a = row(agreement);
|
|
11
|
+
const payer = row(a.parties).payer;
|
|
12
|
+
const principal = typeof payer === 'string' ? payer : row(payer).principal;
|
|
13
|
+
return (principal === 'everyone' || principal === 'org') && a.engagementKind !== 'link';
|
|
14
|
+
}
|
|
2
15
|
/** Enrich each wake from the agreement it actually names, never from the room's
|
|
3
16
|
* latest deal. Reads use the agent's credentials; no grants are inferred here. */
|
|
4
17
|
export async function withAgreementContext(event, read) {
|
|
@@ -20,13 +33,20 @@ export async function withAgreementContext(event, read) {
|
|
|
20
33
|
const terms = row(agreement.terms);
|
|
21
34
|
const usage = row(agreement.conversationUsage);
|
|
22
35
|
const money = row(agreement.money);
|
|
36
|
+
if (isListing(agreement)) {
|
|
37
|
+
return {
|
|
38
|
+
agreementId: id, available: true, listing: true, relationship: true,
|
|
39
|
+
status: agreement.status,
|
|
40
|
+
scope: terms.description,
|
|
41
|
+
instruction: 'This is a store listing, not work: a stranger talks with you under it, and hires you by claiming it. Answer as conversation; nothing you do is spent under it.',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
23
44
|
const limit = typeof terms.conversationAllowance === 'number' ? terms.conversationAllowance : null;
|
|
24
45
|
const used = typeof usage.messages === 'number' ? usage.messages : 0;
|
|
25
46
|
return {
|
|
26
47
|
agreementId: id, available: true,
|
|
27
|
-
freeConversation: row(agreement.metadata).freeConversation === true,
|
|
28
48
|
status: agreement.status,
|
|
29
|
-
relationship: agreement.engagementKind === 'link'
|
|
49
|
+
relationship: agreement.engagementKind === 'link',
|
|
30
50
|
interactionMode: terms.interactionMode ?? 'both',
|
|
31
51
|
parentAgreementId: agreement.parentAgreementId ?? null,
|
|
32
52
|
allowanceScope: terms.conversationAllowanceScope ?? 'direct',
|
|
@@ -65,11 +85,11 @@ export async function withAgreementContext(event, read) {
|
|
|
65
85
|
};
|
|
66
86
|
return enrich(event);
|
|
67
87
|
}
|
|
68
|
-
/**
|
|
88
|
+
/** A link or a listing supplies conversation context, never consent to another engagement. */
|
|
69
89
|
export function permitsAutomaticMandate(event) {
|
|
70
90
|
const frame = row(event);
|
|
71
91
|
if (frame.type === 'batch' && Array.isArray(frame.events))
|
|
72
92
|
return frame.events.every(permitsAutomaticMandate);
|
|
73
93
|
const context = row(frame.agreementContext);
|
|
74
|
-
return context.relationship !== true && context.
|
|
94
|
+
return context.relationship !== true && context.available !== false;
|
|
75
95
|
}
|
|
@@ -36,15 +36,6 @@ export interface EmittedEvent {
|
|
|
36
36
|
senderId?: string;
|
|
37
37
|
[key: string]: unknown;
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
40
|
-
* A chat message whose stated receiver is a DIFFERENT party. `receiverId` is
|
|
41
|
-
* the sender's addressing decision, carried on every wire frame; the
|
|
42
|
-
* human-broadcast sentinel counts as "elsewhere" for an agent (a broadcast is
|
|
43
|
-
* addressed to the room's humans). A frame with NO receiver keeps the turn —
|
|
44
|
-
* we cannot know it is not ours, and skipping on absence would deafen agents
|
|
45
|
-
* to malformed or legacy frames.
|
|
46
|
-
*/
|
|
47
|
-
export declare function isMessageAddressedElsewhere(ev: RawEvent | null | undefined, ownAgentId: string | null | undefined): boolean;
|
|
48
39
|
/**
|
|
49
40
|
* Converts a raw incoming event into the single Outcome that re-enters the
|
|
50
41
|
* machine.
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { isProtocolToolName, mapProtocolToolToOperation } from '../tasks/protocolRegistry.js';
|
|
2
2
|
import { getBatchEvents, isTaskResultRelevantToAgent } from '../context/batch.js';
|
|
3
3
|
import { bookForDelegation, ensureTaskBook, findBookHoldingPending, removeTaskBook, } from './taskBookkeeping.js';
|
|
4
|
-
/**
|
|
5
|
-
* A chat message whose stated receiver is a DIFFERENT party. `receiverId` is
|
|
6
|
-
* the sender's addressing decision, carried on every wire frame; the
|
|
7
|
-
* human-broadcast sentinel counts as "elsewhere" for an agent (a broadcast is
|
|
8
|
-
* addressed to the room's humans). A frame with NO receiver keeps the turn —
|
|
9
|
-
* we cannot know it is not ours, and skipping on absence would deafen agents
|
|
10
|
-
* to malformed or legacy frames.
|
|
11
|
-
*/
|
|
12
|
-
export function isMessageAddressedElsewhere(ev, ownAgentId) {
|
|
13
|
-
if (!ev || ev.type !== 'message' || !ownAgentId || !ev.receiverId) {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
if (ev.assigneeId === ownAgentId || ev.receiverId === ownAgentId)
|
|
17
|
-
return false;
|
|
18
|
-
return true;
|
|
19
|
-
}
|
|
20
4
|
// ── outcomeFromEvent: incoming wire event → Outcome ──────────────────────────
|
|
21
5
|
/**
|
|
22
6
|
* Converts a raw incoming event into the single Outcome that re-enters the
|
|
@@ -168,14 +152,6 @@ function singleEventToOutcome(ev, ownAgentId) {
|
|
|
168
152
|
// Suppress our own messages echoed back through the chat fanout.
|
|
169
153
|
if (ownAgentId && ev.senderId && ev.senderId === ownAgentId)
|
|
170
154
|
return null;
|
|
171
|
-
// Attention gate: a message addressed to someone else is room context, not
|
|
172
|
-
// a turn. Delivery is audience-based on purpose (everyone in the chat hears
|
|
173
|
-
// everything), so in a multi-agent room every agent used to burn an LLM turn
|
|
174
|
-
// per utterance just to decide it wasn't theirs — the sender already decided
|
|
175
|
-
// that by naming a receiver. The message still lands in chat history and
|
|
176
|
-
// rides in on the next wake's read-back; nothing is lost but the interrupt.
|
|
177
|
-
if (isMessageAddressedElsewhere(ev, ownAgentId))
|
|
178
|
-
return null;
|
|
179
155
|
const text = typeof ev.text === 'string' ? ev.text : undefined;
|
|
180
156
|
const senderType = typeof ev.senderType === 'string' ? ev.senderType : undefined;
|
|
181
157
|
return { kind: 'message-received', text, senderId: ev.senderId, senderType };
|
package/dist/runtime/kickoff.js
CHANGED
|
@@ -175,16 +175,16 @@ export function buildClaimKickoffState(initial) {
|
|
|
175
175
|
return {
|
|
176
176
|
kind: 'thinking',
|
|
177
177
|
prompt: {
|
|
178
|
-
role:
|
|
179
|
-
goal:
|
|
180
|
-
constraints:
|
|
178
|
+
role: "A buyer just claimed your store listing. chat_open with the buyer and the claimed agreementId returns the hire's room, which the server usually made at claim, or opens one when there is none. Then greet them and start serving the terms.",
|
|
179
|
+
goal: "Open or reuse the hire's room with the buyer and greet them so the engagement begins.",
|
|
180
|
+
constraints: "The claimed agreement id is in <event> / activeAgreementId. The buyer is the claim actor (Sender / actorId / Buyer on the event) — call chat_open with that participantId AND the claimed agreementId. That returns the hire's room the server usually made at claim, or opens one when there is none. Then chat_send a short greeting that acknowledges the hire and offers to serve the agreement terms. Do not chat_send to an agrn-* session id. Do not spawn task_create here unless the terms require it after the greeting. Do not re-negotiate — the agreement is already active.",
|
|
181
181
|
},
|
|
182
182
|
actions: {
|
|
183
183
|
...dflt.actions,
|
|
184
184
|
openChat: {
|
|
185
185
|
prompt: {
|
|
186
|
-
when:
|
|
187
|
-
instruction:
|
|
186
|
+
when: "Call chat_open with the buyer and the claimed agreementId. The server usually already made the hire's room at claim; this returns it, or opens one when there is none.",
|
|
187
|
+
instruction: "Open or reuse the hire's room with the buyer who claimed the listing.",
|
|
188
188
|
format: '{"participantId": "<buyer actorId / Sender from the event>", "agreementId": "<the claimed agreement id>"}',
|
|
189
189
|
},
|
|
190
190
|
tool: 'chat_open',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { agreementIdFromLane } from '@ziggs-ai/api-client';
|
|
2
|
+
import { isListing } from './agreementContext.js';
|
|
2
3
|
const record = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
3
4
|
const id = (value) => typeof value === 'string' && value.trim() ? value : undefined;
|
|
4
5
|
/** Never combine a task from one event with an agreement from another. */
|
|
@@ -22,7 +23,10 @@ export function workScopeFromWake(event, sessionId) {
|
|
|
22
23
|
}
|
|
23
24
|
const resource = record(rec.resource);
|
|
24
25
|
const task = rec.type === 'task_result' || rec.type === 'notification' ? record(rec.result) : {};
|
|
25
|
-
|
|
26
|
+
// A listing is never work, so it is never what a wake's sends and
|
|
27
|
+
// tools are filed under.
|
|
28
|
+
const listing = record(rec.agreementContext).listing === true;
|
|
29
|
+
const agreementId = listing ? undefined : id(rec.agreementId) ?? id(resource.agreementId) ?? id(task.agreementId)
|
|
26
30
|
?? agreementIdFromLane(rec.chatId) ?? agreementIdFromLane(resource.chatId)
|
|
27
31
|
?? agreementIdFromLane(sessionId) ?? undefined;
|
|
28
32
|
const taskId = id(rec.taskId) ?? id(resource.taskId) ?? id(task.taskId) ?? (resource.kind === 'task' ? id(resource.resourceId) : undefined);
|
|
@@ -100,6 +104,10 @@ export class WakeEffects {
|
|
|
100
104
|
if (effect.kind === 'read-context' && effect.sessionId === this.sessionId
|
|
101
105
|
&& outer.chatMissing !== true && Array.isArray(outer.agreements)) {
|
|
102
106
|
for (const row of outer.agreements) {
|
|
107
|
+
// A stranger's room is linked to the agent's listing; the listing is
|
|
108
|
+
// every stranger's and never the room's work.
|
|
109
|
+
if (isListing(row))
|
|
110
|
+
continue;
|
|
103
111
|
const agreementId = id(record(row).agreementId);
|
|
104
112
|
if (agreementId)
|
|
105
113
|
this.roomAgreementIds.push(agreementId);
|
package/dist/types.d.ts
CHANGED
|
@@ -207,6 +207,8 @@ export interface Ctx {
|
|
|
207
207
|
lastOutcome: Outcome;
|
|
208
208
|
/** Claude Agent SDK session id for resume across ticks (claude-sdk cognition only). */
|
|
209
209
|
claudeSessionId?: string;
|
|
210
|
+
/** Work and audience bound to the saved transcript. Older unbound sessions are not resumed. */
|
|
211
|
+
claudeWorkScope?: string;
|
|
210
212
|
/**
|
|
211
213
|
* Recent wake timestamps (epoch ms) for this lane (claude-sdk cognition
|
|
212
214
|
* only). Backstop circuit breaker: if a lane wakes faster than any real
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.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",
|
|
@@ -23,10 +23,10 @@
|
|
|
23
23
|
"test:watch": "node --experimental-test-module-mocks --import tsx/esm --test --watch test/*.test.ts"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
26
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.284",
|
|
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.29.0",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|