@ziggs-ai/agent-sdk 0.30.0 → 0.32.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 +34 -2
- package/dist/brain/claudeCognition.js +5 -16
- package/dist/brain/claudeSdkProtocol.js +1 -0
- package/dist/brain/claudeSdkTools.js +2 -2
- package/dist/brain/hostedToolSurface.js +17 -3
- package/dist/runtime/PromptBuilder.d.ts +1 -1
- package/dist/runtime/PromptBuilder.js +9 -16
- package/dist/runtime/runTurn.js +22 -10
- package/package.json +2 -2
package/dist/Agent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { withAgreementContext } from './runtime/agreementContext.js';
|
|
2
|
-
import { agreementLaneId, agreementIdFromLane } from '@ziggs-ai/api-client';
|
|
2
|
+
import { agreementLaneId, agreementIdFromLane, isContextAccessRequest } from '@ziggs-ai/api-client';
|
|
3
3
|
import { createDeclaredPollRunner } from './platform/polls/declaredPollRunner.js';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -515,6 +515,29 @@ export class Agent {
|
|
|
515
515
|
const ownAgentId = this.options.agentId;
|
|
516
516
|
if (!ownAgentId)
|
|
517
517
|
return;
|
|
518
|
+
// A permission decision is a notice about a grant, not authority to run
|
|
519
|
+
// work under the (now fulfilled/cancelled) request. Resume the hire that
|
|
520
|
+
// the server recorded when it validated the request; ordinary read/write
|
|
521
|
+
// gates still recheck that hire. Ignore the duplicate lifecycle echo.
|
|
522
|
+
let resumeAgreementId;
|
|
523
|
+
const reason = typeof event.reason === 'string' ? event.reason : '';
|
|
524
|
+
if (event.kind === 'agreement' && /^(context_request_|approval:|status:cancelled)/.test(reason)) {
|
|
525
|
+
const requestId = String(event.resourceId || event.agreementId || '');
|
|
526
|
+
try {
|
|
527
|
+
const row = await this.agreementService.get(requestId);
|
|
528
|
+
const request = row?.grantRequest;
|
|
529
|
+
if (isContextAccessRequest(request) && typeof request.resumeAgreementId === 'string' && request.resumeAgreementId) {
|
|
530
|
+
if (!reason.startsWith('context_request_'))
|
|
531
|
+
return;
|
|
532
|
+
resumeAgreementId = request.resumeAgreementId;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch {
|
|
536
|
+
// A revoked or unavailable agreement must not pin the inbox cursor.
|
|
537
|
+
// Route the original event; normal tool authorization still applies.
|
|
538
|
+
runtimeLog.debug('Agent', 'Access-decision lookup unavailable; using original event');
|
|
539
|
+
}
|
|
540
|
+
}
|
|
518
541
|
// Map the event to a candidate session key using the configured resolver
|
|
519
542
|
// on a synthetic normalized-event shape. Default resolver buckets by
|
|
520
543
|
// chatId; agents using a different sessionResolver (agreementId, taskId,
|
|
@@ -543,7 +566,8 @@ export class Agent {
|
|
|
543
566
|
if (!this.sessions.has(laneKey)) {
|
|
544
567
|
this.sessions.set(laneKey, { sessionId, ownAgentId });
|
|
545
568
|
}
|
|
546
|
-
const agreementId =
|
|
569
|
+
const agreementId = resumeAgreementId ||
|
|
570
|
+
(typeof event.agreementId === 'string' && event.agreementId) ||
|
|
547
571
|
(event.kind === 'agreement' && typeof event.resourceId === 'string'
|
|
548
572
|
? event.resourceId
|
|
549
573
|
: undefined);
|
|
@@ -560,6 +584,14 @@ export class Agent {
|
|
|
560
584
|
}
|
|
561
585
|
/** Wire entry. Normalize → resolve session → enqueue on the right lane. */
|
|
562
586
|
async handleMessage(text, metadata = {}) {
|
|
587
|
+
// Permission decisions also arrive as a chat notification. The dedicated
|
|
588
|
+
// context_request_* resource event owns continuation; processing this echo
|
|
589
|
+
// would answer twice, once under the permission request instead of the hire.
|
|
590
|
+
const notice = metadata.service;
|
|
591
|
+
if (metadata.entryType === 'notification' &&
|
|
592
|
+
['proposal_approved', 'proposal_rejected'].includes(notice?.operation ?? '') &&
|
|
593
|
+
isContextAccessRequest(notice?.agreement?.grantRequest))
|
|
594
|
+
return;
|
|
563
595
|
const messageId = metadata.messageId;
|
|
564
596
|
if (messageId && this.seenMessages.has(messageId)) {
|
|
565
597
|
runtimeLog.debug('Agent', `skip duplicate messageId=${messageId}`);
|
|
@@ -224,7 +224,8 @@ function renderEventLines(event, lines, now) {
|
|
|
224
224
|
const decisionActor = (typeof resource.actorId === 'string' && resource.actorId) ||
|
|
225
225
|
(typeof event.actorId === 'string' && event.actorId) ||
|
|
226
226
|
null;
|
|
227
|
-
const decisionAgreementId = (typeof
|
|
227
|
+
const decisionAgreementId = (resource.kind === 'agreement' && typeof resource.resourceId === 'string' && resource.resourceId) ||
|
|
228
|
+
(typeof event.agreementId === 'string' && event.agreementId) ||
|
|
228
229
|
(typeof resource.agreementId === 'string' && resource.agreementId) ||
|
|
229
230
|
(resource.kind === 'agreement' &&
|
|
230
231
|
typeof resource.resourceId === 'string' &&
|
|
@@ -238,11 +239,11 @@ function renderEventLines(event, lines, now) {
|
|
|
238
239
|
surface: 'sdk',
|
|
239
240
|
});
|
|
240
241
|
if (reason === 'context_request_fulfilled' || reason === 'context_request_rejected') {
|
|
241
|
-
lines.push('Work signal: the owner answered your
|
|
242
|
+
lines.push('Work signal: the owner answered your context access request. ' +
|
|
242
243
|
(decision ? `${decision.sentence} ` : '') +
|
|
243
|
-
'Read the
|
|
244
|
+
'Read the permission request and this conversation to recover the pending work. The wake agreementId is the ongoing hire to act under; the resourceId identifies the permission decision. ' +
|
|
244
245
|
(reason === 'context_request_fulfilled'
|
|
245
|
-
? '
|
|
246
|
+
? 'Read the original resource using the approved grant (context_read for a workspace), verify the read succeeds, and deliver the requested answer to its original recipient. '
|
|
246
247
|
: 'Access was rejected: report the unresolved dependency to the original requester. ') +
|
|
247
248
|
'This is an access decision, not a new hire; do not create a new task or repeat your introduction.');
|
|
248
249
|
}
|
|
@@ -286,18 +287,6 @@ function renderEventLines(event, lines, now) {
|
|
|
286
287
|
lines.push(`Agreement: ${agreementId}`);
|
|
287
288
|
}
|
|
288
289
|
}
|
|
289
|
-
// The other side of the same policy: a message this agent sent to a
|
|
290
|
-
// published agent landed and was NOT rung through. Nothing is wrong and
|
|
291
|
-
// nothing is lost, so this must not read as a refusal — but without it the
|
|
292
|
-
// sender waits on a reply that is never coming and its rational next move
|
|
293
|
-
// is to send again.
|
|
294
|
-
if (reason === 'contact_policy_withheld') {
|
|
295
|
-
lines.push('Your message was delivered and is readable, but the agent you wrote ' +
|
|
296
|
-
'to is not woken for it: its listing is claim-first, so it starts ' +
|
|
297
|
-
'work once you claim the listing. Do not resend — claim the listing ' +
|
|
298
|
-
'(ziggs_marketplace_view to find it, then ziggs_agreement_claim), or ' +
|
|
299
|
-
'wait if somebody else is claiming on your behalf.');
|
|
300
|
-
}
|
|
301
290
|
}
|
|
302
291
|
if (event?.agreementContext) {
|
|
303
292
|
lines.push(`Service agreement context (server read): ${JSON.stringify(event.agreementContext)}`);
|
|
@@ -27,6 +27,7 @@ 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
|
+
- Use any room you can write in, or open one (newChat for a separate subject). The server refuses what you are not allowed to do, and the refusal says why.
|
|
30
31
|
- 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
32
|
- 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
33
|
- Hand off by completing your task — the platform notifies the next agent.
|
|
@@ -70,11 +70,11 @@ export const contextSnapshotTool = defineTool('context_snapshot', {
|
|
|
70
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
|
-
conversationAgreementId: { type: 'string', description: '
|
|
73
|
+
conversationAgreementId: { type: 'string', description: 'The agreement this message is under. In the room you were woken in, leave it out unless you mean a different agreement than this wake. When a send is refused with WORK_SELECTION_REQUIRED, name one of its choices.' },
|
|
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
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
|
-
receiverId: { type: 'string', description: '
|
|
77
|
+
receiverId: { type: 'string', description: 'Name someone only when you mean them. Leave it out to write back in this room: one other participant is the recipient, and with several Jev may select one. Otherwise the message stays in the room with no specific wake. To name a masked party, pass its rpb_ ref as it came. Pass human to address the people in the room.' },
|
|
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) => {
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* The tools stay registered on the ToolManager — the FSM brain and the
|
|
9
9
|
* dispatcher both call them — they just stop riding every wake as schemas.
|
|
10
10
|
*/
|
|
11
|
-
import { EVERYDAY_SDK_TOOLS, annotationForSdkTool } from '@ziggs-ai/api-client';
|
|
11
|
+
import { EVERYDAY_SDK_TOOLS, annotationForSdkTool, backendToolPick } from '@ziggs-ai/api-client';
|
|
12
|
+
import { credsFromToolContext } from '../tools/toolCreds.js';
|
|
12
13
|
import { defineTool, toolName } from '../tools/defineTool.js';
|
|
13
14
|
/**
|
|
14
15
|
* Platform tools written by hand, not as a shared capability. Their lane is
|
|
@@ -132,6 +133,7 @@ async function callCatalogTarget(name, payload, ctx, execute) {
|
|
|
132
133
|
function catalogTools(present, execute) {
|
|
133
134
|
const list = () => [...present.values()].map(rowFor).sort((a, b) => a.name.localeCompare(b.name));
|
|
134
135
|
const toolsTool = defineTool('tools', {
|
|
136
|
+
need: { type: 'string', description: 'Describe what you need to do. Suggests a tool and its schema; falls back to the catalog when unsure.' },
|
|
135
137
|
search: {
|
|
136
138
|
type: 'string',
|
|
137
139
|
description: 'Filter by name or the one-line description. Omit to list every tool.',
|
|
@@ -141,7 +143,7 @@ function catalogTools(present, execute) {
|
|
|
141
143
|
items: { type: 'string' },
|
|
142
144
|
description: 'Tool names whose full schema you want before calling them.',
|
|
143
145
|
},
|
|
144
|
-
}, async (args) => {
|
|
146
|
+
}, async (args, ctx) => {
|
|
145
147
|
const describe = Array.isArray(args.describe) ? args.describe.map(String) : [];
|
|
146
148
|
if (describe.length > 0) {
|
|
147
149
|
return describe.map((name) => {
|
|
@@ -152,10 +154,22 @@ function catalogTools(present, execute) {
|
|
|
152
154
|
return { ...rowFor(tool), schema: tool.schema.function };
|
|
153
155
|
});
|
|
154
156
|
}
|
|
157
|
+
const need = typeof args.need === 'string' ? args.need.trim() : '';
|
|
158
|
+
if (need) {
|
|
159
|
+
const rows = list();
|
|
160
|
+
try {
|
|
161
|
+
const pick = await backendToolPick(credsFromToolContext(ctx, 'tools'))(need, rows.map(({ name, line }) => ({ name, line })));
|
|
162
|
+
const tool = pick?.how === 'pick' && pick.tool ? present.get(pick.tool) : undefined;
|
|
163
|
+
if (tool)
|
|
164
|
+
return { suggestion: { ...rowFor(tool), schema: tool.schema.function }, confidence: pick?.confidence };
|
|
165
|
+
}
|
|
166
|
+
catch { /* Missing credentials or picker failure still leaves every tool available. */ }
|
|
167
|
+
return { tools: rows, note: 'No confident suggestion. Choose from the catalog; no action was taken.' };
|
|
168
|
+
}
|
|
155
169
|
const q = typeof args.search === 'string' ? args.search.toLowerCase() : '';
|
|
156
170
|
return list().filter((row) => !q || row.name.includes(q) || row.line.toLowerCase().includes(q));
|
|
157
171
|
}, {
|
|
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
|
|
172
|
+
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 need for a plain-language tool suggestion with its schema, or describe with exact names. Suggestions never execute tools. Nothing is hidden.',
|
|
159
173
|
});
|
|
160
174
|
const dispatch = (via) => async (args, ctx) => {
|
|
161
175
|
const name = typeof args.tool === 'string' ? args.tool : '';
|
|
@@ -89,7 +89,7 @@ export declare class PromptBuilder {
|
|
|
89
89
|
_renderAgreements(agreements: AnyObj[]): string;
|
|
90
90
|
_renderRecipients(users: AnyObj[], otherAgents: AnyObj[], agreements: AnyObj[], myAgentId: string | null | undefined): string;
|
|
91
91
|
_renderDecisionSchemaFromActions(actions: Record<string, Action>, senderMeta: SenderMeta, ctxObj: ServerContext, omitMessage?: boolean): string;
|
|
92
|
-
_buildActionSchema(name: string,
|
|
92
|
+
_buildActionSchema(name: string, omitMessage?: boolean): string;
|
|
93
93
|
_getSenderMeta(event: RawEvent | AnyObj | null, ctxObj: ServerContext): SenderMeta;
|
|
94
94
|
_getMyAgentId(ctxObj: ServerContext): string | null;
|
|
95
95
|
}
|
|
@@ -322,9 +322,10 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
|
|
|
322
322
|
}
|
|
323
323
|
}
|
|
324
324
|
// Chat keeps the full directory — including opaque `rpb_*` / `psn_*` refs
|
|
325
|
-
// the persona layer may surface.
|
|
326
|
-
// backend resolves
|
|
327
|
-
// never list opaque refs — lookup / wake / pay
|
|
325
|
+
// the persona layer may surface. To name a masked party, pass its `rpb_`
|
|
326
|
+
// ref as it came; the backend resolves it in-room. Transfer (and any
|
|
327
|
+
// account-id action) must never list opaque refs — lookup / wake / pay
|
|
328
|
+
// against them 400s.
|
|
328
329
|
const chatIds = new Set([...userIds, ...agentIds]);
|
|
329
330
|
const transferIds = new Set([...userIds, ...agentIds].filter((id) => !isOpaquePresentationRef(id)));
|
|
330
331
|
const lines = ['<recipients>'];
|
|
@@ -381,8 +382,8 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
|
|
|
381
382
|
const lines = ['<decision_schema>'];
|
|
382
383
|
lines.push('Return ONE JSON object. ALWAYS include "thought" as the FIRST field — briefly explain your reasoning before choosing an action.');
|
|
383
384
|
lines.push('Set "action" to exactly one of the action names below.');
|
|
385
|
+
lines.push('Name "receiverId" only when you mean a specific recipient. Leave it out to write back in this room; an answer without one is sent without one.');
|
|
384
386
|
lines.push('Message text is rendered as markdown (CommonMark + GFM). You MAY use **bold**, *italic*, `code`, fenced code blocks, lists, links, and tables when they improve clarity — plain text is also fine.\n');
|
|
385
|
-
const replyTo = '<receiverId from <recipients>>';
|
|
386
387
|
const pendingProposals = (ctxObj.agreements || []).filter((a) => a.proposal?.status === 'pending' && (a.proposedToIsYou || isBroadcastTarget(a.parties?.proposedTo?.principal)));
|
|
387
388
|
if (pendingProposals.length > 0) {
|
|
388
389
|
lines.push(`PRIORITY: You have ${pendingProposals.length} pending proposal(s) directed at you. Respond BEFORE other actions.`);
|
|
@@ -402,7 +403,7 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
|
|
|
402
403
|
lines.push(`Response: {"thought": "...", "action": "${name}", "args": {...}}`);
|
|
403
404
|
}
|
|
404
405
|
else {
|
|
405
|
-
lines.push(this._buildActionSchema(name,
|
|
406
|
+
lines.push(this._buildActionSchema(name, omitMessage));
|
|
406
407
|
}
|
|
407
408
|
if (actionDef.prompt?.examples?.length) {
|
|
408
409
|
lines.push('Examples:');
|
|
@@ -417,22 +418,14 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessage
|
|
|
417
418
|
lines.push('</decision_schema>');
|
|
418
419
|
return lines.join('\n');
|
|
419
420
|
}
|
|
420
|
-
_buildActionSchema(name,
|
|
421
|
+
_buildActionSchema(name, omitMessage) {
|
|
421
422
|
if (name === 'wait') {
|
|
422
423
|
return `Response: {"thought": "...", "action": "${name}"}`;
|
|
423
424
|
}
|
|
424
|
-
// Never pre-fill a non-routable id (the literal 'system' or 'service'
|
|
425
|
-
// buckets) as the default receiverId. Fall back to an explicit placeholder
|
|
426
|
-
// so the model picks a real recipient from <recipients>.
|
|
427
|
-
const isSystemReplyTo = !replyTo ||
|
|
428
|
-
replyTo === 'system' ||
|
|
429
|
-
replyTo === 'service' ||
|
|
430
|
-
replyTo.startsWith('<');
|
|
431
|
-
const renderedReplyTo = isSystemReplyTo ? '<receiverId from <recipients>>' : replyTo;
|
|
432
425
|
if (omitMessage) {
|
|
433
|
-
return `Response: {"thought": "...", "action": "${name}"
|
|
426
|
+
return `Response: {"thought": "...", "action": "${name}"}`;
|
|
434
427
|
}
|
|
435
|
-
return `Response: {"thought": "...", "action": "${name}", "
|
|
428
|
+
return `Response: {"thought": "...", "action": "${name}", "message": "..."}`;
|
|
436
429
|
}
|
|
437
430
|
_getSenderMeta(event, ctxObj) {
|
|
438
431
|
const ev = event;
|
package/dist/runtime/runTurn.js
CHANGED
|
@@ -450,7 +450,7 @@ export async function runTurn(input) {
|
|
|
450
450
|
repromptedForDecision = true;
|
|
451
451
|
runtimeLog.warn('runTurn', `unexecutable decision — reprompting once: action "${chosenAction}" missing required fields`);
|
|
452
452
|
messages.push({ role: 'assistant', content: llmResponse.content ?? '' });
|
|
453
|
-
messages.push({ role: 'user', content: `Your last output was not a valid action: action "${chosenAction}" was missing required fields (for a message this means
|
|
453
|
+
messages.push({ role: 'user', content: `Your last output was not a valid action: action "${chosenAction}" was missing required fields (for a message this means "message"). Respond with exactly one action from the schema.` });
|
|
454
454
|
continue;
|
|
455
455
|
}
|
|
456
456
|
const events = (Array.isArray(result) ? result : [result]);
|
|
@@ -666,7 +666,7 @@ async function executeDecision(decision, actionDef, eff, sessionId, memory, inco
|
|
|
666
666
|
if (decision.messages && Array.isArray(decision.messages) && decision.messages.length > 0) {
|
|
667
667
|
return await executeSendMessages(decision, eff, sessionId);
|
|
668
668
|
}
|
|
669
|
-
if (decision.message
|
|
669
|
+
if (decision.message) {
|
|
670
670
|
return await executeSendMessage(decision, eff, sessionId);
|
|
671
671
|
}
|
|
672
672
|
// Fallback: LLM may place message/receiverId inside args due to format ambiguity
|
|
@@ -707,7 +707,8 @@ function isReplySlotTaken(err) {
|
|
|
707
707
|
return err?.status === 409;
|
|
708
708
|
}
|
|
709
709
|
async function executeSendMessage(decision, eff, sessionId) {
|
|
710
|
-
// Callers
|
|
710
|
+
// Callers guarantee message text. receiverId is set only when the model
|
|
711
|
+
// named someone; an answer without one is sent without one. sessionId is
|
|
711
712
|
// always ctx.identity.sessionId. A send failure throws to the turn loop,
|
|
712
713
|
// which converts it into the 'send-failed' error outcome — no silent nulls,
|
|
713
714
|
// and message_sent is only emitted after the send effect resolved.
|
|
@@ -716,16 +717,22 @@ async function executeSendMessage(decision, eff, sessionId) {
|
|
|
716
717
|
// to the current session otherwise.
|
|
717
718
|
const targetSessionId = decision.chatId || sessionId;
|
|
718
719
|
const safeMessage = truncateMsg(decision.message);
|
|
720
|
+
const receiverId = decision.receiverId || undefined;
|
|
719
721
|
try {
|
|
720
|
-
await eff({
|
|
722
|
+
await eff({
|
|
723
|
+
kind: 'send-message',
|
|
724
|
+
sessionId: targetSessionId,
|
|
725
|
+
...(receiverId ? { receiverId } : {}),
|
|
726
|
+
text: safeMessage,
|
|
727
|
+
});
|
|
721
728
|
}
|
|
722
729
|
catch (err) {
|
|
723
730
|
if (!isReplySlotTaken(err))
|
|
724
731
|
throw err;
|
|
725
|
-
runtimeLog.warn('runTurn', `reply already persisted for this wake (idempotency 409) — a replay or second host answered first; dropping duplicate to ${
|
|
726
|
-
return [{ type: 'message_duplicate_skipped', receiverId:
|
|
732
|
+
runtimeLog.warn('runTurn', `reply already persisted for this wake (idempotency 409) — a replay or second host answered first; dropping duplicate${receiverId ? ` to ${receiverId}` : ''}`);
|
|
733
|
+
return [{ type: 'message_duplicate_skipped', ...(receiverId ? { receiverId } : {}), chatId: targetSessionId, senderId: 'system' }];
|
|
727
734
|
}
|
|
728
|
-
const events = [{ type: 'message_sent', receiverId:
|
|
735
|
+
const events = [{ type: 'message_sent', ...(receiverId ? { receiverId } : {}), chatId: targetSessionId, message: safeMessage, senderId: 'system' }];
|
|
729
736
|
if (targetSessionId !== sessionId)
|
|
730
737
|
events.push({ type: 'report_sent', message: 'Reported to owner.', senderId: 'system' });
|
|
731
738
|
return events;
|
|
@@ -744,15 +751,20 @@ async function executeSendMessages(decision, eff, sessionId) {
|
|
|
744
751
|
const failures = [];
|
|
745
752
|
let attempted = 0;
|
|
746
753
|
for (const msg of decision.messages ?? []) {
|
|
747
|
-
if (!msg.
|
|
748
|
-
failures.push({ receiverId: msg.receiverId || '<missing>', error: 'missing
|
|
754
|
+
if (!msg.message) {
|
|
755
|
+
failures.push({ receiverId: msg.receiverId || '<missing>', error: 'missing message' });
|
|
749
756
|
continue;
|
|
750
757
|
}
|
|
751
758
|
const targetSessionId = msg.chatId || sessionId;
|
|
752
759
|
const safeMessage = truncateMsg(msg.message);
|
|
753
760
|
attempted++;
|
|
754
761
|
try {
|
|
755
|
-
await eff({
|
|
762
|
+
await eff({
|
|
763
|
+
kind: 'send-message',
|
|
764
|
+
sessionId: targetSessionId,
|
|
765
|
+
...(msg.receiverId ? { receiverId: msg.receiverId } : {}),
|
|
766
|
+
text: safeMessage,
|
|
767
|
+
});
|
|
756
768
|
}
|
|
757
769
|
catch (err) {
|
|
758
770
|
if (isReplySlotTaken(err)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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.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.31.0",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|