@ziggs-ai/agent-sdk 0.31.0 → 0.33.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 +81 -15
- 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/mcp/createMcpAgent.js +6 -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';
|
|
@@ -463,8 +463,10 @@ export class Agent {
|
|
|
463
463
|
task = await this.taskService.getTask(taskId);
|
|
464
464
|
}
|
|
465
465
|
catch (error) {
|
|
466
|
+
// Unreadable is not a hold. The wake filter still drops a frame whose
|
|
467
|
+
// own task cannot be read; a tool that names one gets an error instead.
|
|
466
468
|
if ([403, 404].includes(error.status ?? 0))
|
|
467
|
-
return { blocked: true };
|
|
469
|
+
return { blocked: true, unread: true };
|
|
468
470
|
throw error;
|
|
469
471
|
}
|
|
470
472
|
// A coordinator sees its children's task events too; those do not pause it.
|
|
@@ -515,6 +517,29 @@ export class Agent {
|
|
|
515
517
|
const ownAgentId = this.options.agentId;
|
|
516
518
|
if (!ownAgentId)
|
|
517
519
|
return;
|
|
520
|
+
// A permission decision is a notice about a grant, not authority to run
|
|
521
|
+
// work under the (now fulfilled/cancelled) request. Resume the hire that
|
|
522
|
+
// the server recorded when it validated the request; ordinary read/write
|
|
523
|
+
// gates still recheck that hire. Ignore the duplicate lifecycle echo.
|
|
524
|
+
let resumeAgreementId;
|
|
525
|
+
const reason = typeof event.reason === 'string' ? event.reason : '';
|
|
526
|
+
if (event.kind === 'agreement' && /^(context_request_|approval:|status:cancelled)/.test(reason)) {
|
|
527
|
+
const requestId = String(event.resourceId || event.agreementId || '');
|
|
528
|
+
try {
|
|
529
|
+
const row = await this.agreementService.get(requestId);
|
|
530
|
+
const request = row?.grantRequest;
|
|
531
|
+
if (isContextAccessRequest(request) && typeof request.resumeAgreementId === 'string' && request.resumeAgreementId) {
|
|
532
|
+
if (!reason.startsWith('context_request_'))
|
|
533
|
+
return;
|
|
534
|
+
resumeAgreementId = request.resumeAgreementId;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// A revoked or unavailable agreement must not pin the inbox cursor.
|
|
539
|
+
// Route the original event; normal tool authorization still applies.
|
|
540
|
+
runtimeLog.debug('Agent', 'Access-decision lookup unavailable; using original event');
|
|
541
|
+
}
|
|
542
|
+
}
|
|
518
543
|
// Map the event to a candidate session key using the configured resolver
|
|
519
544
|
// on a synthetic normalized-event shape. Default resolver buckets by
|
|
520
545
|
// chatId; agents using a different sessionResolver (agreementId, taskId,
|
|
@@ -543,7 +568,8 @@ export class Agent {
|
|
|
543
568
|
if (!this.sessions.has(laneKey)) {
|
|
544
569
|
this.sessions.set(laneKey, { sessionId, ownAgentId });
|
|
545
570
|
}
|
|
546
|
-
const agreementId =
|
|
571
|
+
const agreementId = resumeAgreementId ||
|
|
572
|
+
(typeof event.agreementId === 'string' && event.agreementId) ||
|
|
547
573
|
(event.kind === 'agreement' && typeof event.resourceId === 'string'
|
|
548
574
|
? event.resourceId
|
|
549
575
|
: undefined);
|
|
@@ -560,6 +586,14 @@ export class Agent {
|
|
|
560
586
|
}
|
|
561
587
|
/** Wire entry. Normalize → resolve session → enqueue on the right lane. */
|
|
562
588
|
async handleMessage(text, metadata = {}) {
|
|
589
|
+
// Permission decisions also arrive as a chat notification. The dedicated
|
|
590
|
+
// context_request_* resource event owns continuation; processing this echo
|
|
591
|
+
// would answer twice, once under the permission request instead of the hire.
|
|
592
|
+
const notice = metadata.service;
|
|
593
|
+
if (metadata.entryType === 'notification' &&
|
|
594
|
+
['proposal_approved', 'proposal_rejected'].includes(notice?.operation ?? '') &&
|
|
595
|
+
isContextAccessRequest(notice?.agreement?.grantRequest))
|
|
596
|
+
return;
|
|
563
597
|
const messageId = metadata.messageId;
|
|
564
598
|
if (messageId && this.seenMessages.has(messageId)) {
|
|
565
599
|
runtimeLog.debug('Agent', `skip duplicate messageId=${messageId}`);
|
|
@@ -1086,7 +1120,7 @@ export class Agent {
|
|
|
1086
1120
|
release();
|
|
1087
1121
|
taskRuns.clear();
|
|
1088
1122
|
};
|
|
1089
|
-
const checkpoint = async (
|
|
1123
|
+
const checkpoint = async (namedTaskId) => {
|
|
1090
1124
|
if (tickAbort.signal.aborted) {
|
|
1091
1125
|
if (tickAbort.signal.reason?.code === 'TASK_CONTROL')
|
|
1092
1126
|
heldTask = tickAbort.signal.reason;
|
|
@@ -1094,27 +1128,59 @@ export class Agent {
|
|
|
1094
1128
|
}
|
|
1095
1129
|
if (heldTask)
|
|
1096
1130
|
throw new Error('Task execution held');
|
|
1097
|
-
const
|
|
1131
|
+
const wakeId = marks.scope.taskId ?? wakeTaskId;
|
|
1132
|
+
const taskId = namedTaskId ?? wakeId;
|
|
1098
1133
|
if (!taskId)
|
|
1099
1134
|
return;
|
|
1100
|
-
|
|
1135
|
+
// Only this wake's own task shares the tick abort. A tool that names
|
|
1136
|
+
// some other task must not be able to cancel the rest of the wake.
|
|
1137
|
+
const ownsWake = namedTaskId == null || namedTaskId === wakeId;
|
|
1138
|
+
if (ownsWake)
|
|
1139
|
+
registerTaskRun(taskId);
|
|
1101
1140
|
const execution = await readControl(taskId);
|
|
1102
1141
|
if (heldTask)
|
|
1103
1142
|
throw new Error('Task execution held');
|
|
1104
|
-
if (execution?.blocked)
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1143
|
+
if (!execution?.blocked)
|
|
1144
|
+
return;
|
|
1145
|
+
if (!ownsWake || execution.unread) {
|
|
1146
|
+
const error = execution.unread
|
|
1147
|
+
? `Task ${taskId} cannot be read. This call did not run.`
|
|
1148
|
+
: execution.closed
|
|
1149
|
+
? `Task ${taskId} is cancelled. This call did not run.`
|
|
1150
|
+
: `Task ${taskId} is paused. This call did not run.`;
|
|
1151
|
+
return { error };
|
|
1109
1152
|
}
|
|
1153
|
+
heldTask = { taskId, control: execution.control };
|
|
1154
|
+
taskRuns.get(taskId).run.control = execution.control;
|
|
1155
|
+
tickAbort.abort(Object.assign(new Error('Task execution held'), { code: 'TASK_CONTROL', taskId, control: execution.control }));
|
|
1156
|
+
throw new Error('Task execution held');
|
|
1157
|
+
};
|
|
1158
|
+
const taskIdNamedBy = (effect) => {
|
|
1159
|
+
if (effect.kind !== 'tool-call' || !effect.args || typeof effect.args !== 'object' || Array.isArray(effect.args))
|
|
1160
|
+
return undefined;
|
|
1161
|
+
const args = effect.args;
|
|
1162
|
+
if (typeof args.taskId === 'string')
|
|
1163
|
+
return args.taskId;
|
|
1164
|
+
const nested = args.args;
|
|
1165
|
+
if ((effect.name === 'tool_read' || effect.name === 'tool_write')
|
|
1166
|
+
&& nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
1167
|
+
const inner = nested.taskId;
|
|
1168
|
+
if (typeof inner === 'string')
|
|
1169
|
+
return inner;
|
|
1170
|
+
}
|
|
1171
|
+
return undefined;
|
|
1110
1172
|
};
|
|
1111
1173
|
const eff = async (effect) => {
|
|
1112
1174
|
// Record-only telemetry can finish after interruption. Every action and
|
|
1113
1175
|
// model call checks the current task before starting another operation.
|
|
1114
|
-
if (effect.kind
|
|
1115
|
-
const
|
|
1116
|
-
|
|
1117
|
-
|
|
1176
|
+
if (effect.kind === 'tool-call') {
|
|
1177
|
+
const refusal = await checkpoint(taskIdNamedBy(effect));
|
|
1178
|
+
// The handler is generic over every effect. This branch is only a tool call.
|
|
1179
|
+
if (refusal)
|
|
1180
|
+
return { ok: false, error: refusal.error };
|
|
1181
|
+
}
|
|
1182
|
+
else if (effect.kind !== 'record-event') {
|
|
1183
|
+
await checkpoint();
|
|
1118
1184
|
}
|
|
1119
1185
|
const agreementId = marks.scope.agreementId;
|
|
1120
1186
|
const sameRoom = 'sessionId' in effect && effect.sessionId === session.sessionId;
|
|
@@ -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 : '';
|
|
@@ -395,14 +395,17 @@ export async function createMcpAgent(options) {
|
|
|
395
395
|
: 'the external service';
|
|
396
396
|
// Open a connection request for this user; returns its id so the caller can
|
|
397
397
|
// deep-link the user straight to it on the Connections page.
|
|
398
|
-
|
|
398
|
+
// The server holds the invoking user to the people this wake may ask, so
|
|
399
|
+
// the request carries the same lane as the call that missed: in the hire's
|
|
400
|
+
// room that user is its customer.
|
|
401
|
+
const triggerConnectionRequest = async (userId, laneId) => {
|
|
399
402
|
if (!transportConfig.serverUrl)
|
|
400
403
|
return null;
|
|
401
404
|
const operatorKey = transportConfig.operatorKey || process.env.ZIGGS_OPERATOR_KEY;
|
|
402
405
|
if (!operatorKey)
|
|
403
406
|
return null;
|
|
404
407
|
try {
|
|
405
|
-
const body = await new ConnectionsClient(operatorKey, agentId, backendBase).requestMcpConnection({
|
|
408
|
+
const body = await new ConnectionsClient(operatorKey, agentId, backendBase, laneId).requestMcpConnection({
|
|
406
409
|
serverUrl: transportConfig.serverUrl,
|
|
407
410
|
tools: tools.map(t => t.name),
|
|
408
411
|
onBehalfOfUserId: userId,
|
|
@@ -460,7 +463,7 @@ export async function createMcpAgent(options) {
|
|
|
460
463
|
if (lastErr instanceof ZiggsGatewayError &&
|
|
461
464
|
lastErr.code === 'NO_USER_CONNECTION' &&
|
|
462
465
|
transportConfig.serverUrl) {
|
|
463
|
-
const requestId = await triggerConnectionRequest(userId).catch(() => null);
|
|
466
|
+
const requestId = await triggerConnectionRequest(userId, laneId).catch(() => null);
|
|
464
467
|
const link = connectionsSettingsAppUrl(appUrl, requestId);
|
|
465
468
|
return {
|
|
466
469
|
content: [
|
|
@@ -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.33.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.32.0",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|