@ziggs-ai/agent-sdk 0.1.3 → 0.1.4

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.
Files changed (85) hide show
  1. package/README.md +1 -1
  2. package/package.json +9 -4
  3. package/src/AgentHost.ts +342 -0
  4. package/src/adapters/OpenAIAdapter.ts +125 -0
  5. package/src/agent/Agent.ts +98 -0
  6. package/src/cognition/validateContext.ts +95 -0
  7. package/src/context/applyEffects.ts +80 -0
  8. package/src/context/batch.ts +17 -0
  9. package/src/context/classifyEnvelope.ts +38 -0
  10. package/src/context/routingLabels.ts +46 -0
  11. package/src/defineAgent.ts +62 -0
  12. package/src/formatters/AgreementFormatter.ts +111 -0
  13. package/src/formatters/HistoryFormatter.ts +166 -0
  14. package/src/formatters/index.ts +2 -0
  15. package/src/index.ts +86 -0
  16. package/src/ingress/normalizeIncoming.ts +119 -0
  17. package/src/memory/MemoryStore.ts +104 -0
  18. package/src/runtime/AgentMachine.ts +298 -0
  19. package/src/runtime/PromptBuilder.ts +461 -0
  20. package/src/runtime/buildOutcome.ts +488 -0
  21. package/src/runtime/defaults.ts +72 -0
  22. package/src/runtime/runTurn.ts +637 -0
  23. package/src/runtime/validateWorkflow.ts +165 -0
  24. package/src/server/ConnectionPool.ts +155 -0
  25. package/src/server/EventQueue.ts +119 -0
  26. package/src/server/OutboxBuffer.ts +90 -0
  27. package/src/server/ZiggsEffectHandler.ts +335 -0
  28. package/src/server/agreements/AgreementService.ts +111 -0
  29. package/src/server/createHealthServer.ts +8 -0
  30. package/src/server/proactive/ProactiveTrigger.ts +83 -0
  31. package/src/server/runLauncher.ts +131 -0
  32. package/src/server/tasks/TaskService.ts +111 -0
  33. package/src/server/tasks/index.ts +4 -0
  34. package/src/server/tasks/paymentTools.ts +156 -0
  35. package/src/server/tasks/protocolRunner.ts +101 -0
  36. package/src/server/tasks/protocolTools.ts +96 -0
  37. package/src/server/ziggspay/ZiggsPayClient.ts +193 -0
  38. package/src/shared/ids.ts +3 -0
  39. package/src/shared/runtimeLog.ts +72 -0
  40. package/src/shared/types.ts +31 -0
  41. package/src/tasks/protocolRegistry.ts +25 -0
  42. package/src/tasks/taskCore.ts +139 -0
  43. package/src/tools/ToolManager.ts +95 -0
  44. package/src/tools/{ToolProvider.js → ToolProvider.ts} +5 -15
  45. package/src/tools/defineTool.ts +90 -0
  46. package/src/tools/index.ts +5 -0
  47. package/src/types.ts +368 -0
  48. package/src/utils/jsonExtractor.ts +100 -0
  49. package/src/ConnectionPool.js +0 -133
  50. package/src/adapters/OpenAIAdapter.js +0 -73
  51. package/src/agent/Agent.js +0 -121
  52. package/src/agent/EventQueue.js +0 -68
  53. package/src/agent/OutboxBuffer.js +0 -62
  54. package/src/cognition/PromptBuilder.js +0 -312
  55. package/src/cognition/resolveActionTool.js +0 -12
  56. package/src/cognition/runTurn.js +0 -578
  57. package/src/context/applyEffects.js +0 -133
  58. package/src/context/batch.js +0 -25
  59. package/src/context/classifyEnvelope.js +0 -82
  60. package/src/context/routingLabels.js +0 -54
  61. package/src/createHealthServer.js +0 -28
  62. package/src/formatters/HistoryFormatter.js +0 -257
  63. package/src/formatters/TaskFormatter.js +0 -180
  64. package/src/formatters/index.js +0 -9
  65. package/src/index.js +0 -76
  66. package/src/ingress/normalizeIncoming.js +0 -70
  67. package/src/runLauncher.js +0 -159
  68. package/src/shared/ids.js +0 -7
  69. package/src/shared/types.js +0 -86
  70. package/src/tasks/TaskService.js +0 -247
  71. package/src/tasks/index.js +0 -9
  72. package/src/tasks/taskCore.js +0 -229
  73. package/src/tasks/taskProtocolRegistry.js +0 -22
  74. package/src/tasks/taskProtocolRunner.js +0 -107
  75. package/src/tasks/taskProtocolTools.js +0 -87
  76. package/src/tools/ToolManager.js +0 -79
  77. package/src/tools/defineTool.js +0 -82
  78. package/src/tools/index.js +0 -11
  79. package/src/utils/jsonExtractor.js +0 -139
  80. package/src/workflow/AgentMachine.js +0 -250
  81. package/src/workflow/WorkflowRuntime.js +0 -63
  82. package/src/workflow/dsl.js +0 -287
  83. package/src/workflow/motifs.js +0 -435
  84. package/src/ziggs/runtime.js +0 -192
  85. /package/src/adapters/{index.js → index.ts} +0 -0
@@ -0,0 +1,461 @@
1
+ import type { Action, Ctx, PromptDef } from '../types.js';
2
+ import { HistoryFormatter, AgreementFormatter } from '../formatters/index.js';
3
+ import { validateContext } from '../cognition/validateContext.js';
4
+ import type { RawEvent } from './buildOutcome.js';
5
+
6
+ type AnyObj = Record<string, unknown>;
7
+
8
+ interface ServerContext {
9
+ history?: AnyObj[];
10
+ agreements?: AnyObj[];
11
+ agents?: AnyObj[];
12
+ users?: AnyObj[];
13
+ [key: string]: unknown;
14
+ }
15
+
16
+ interface ServerContextBundle {
17
+ context: ServerContext;
18
+ tools: AnyObj[];
19
+ }
20
+
21
+ interface SenderMeta {
22
+ senderId?: string;
23
+ senderType?: string;
24
+ [key: string]: unknown;
25
+ }
26
+
27
+ interface BuildArgs {
28
+ statePrompt: PromptDef;
29
+ actions: Record<string, Action>;
30
+ serverContext: ServerContextBundle;
31
+ incomingEvent: RawEvent | null;
32
+ definition: { description?: string };
33
+ chatId: string | null;
34
+ stateId: string;
35
+ ctx: Ctx;
36
+ }
37
+
38
+ interface HistoryFormatterI { format(history: unknown[] | null | undefined, agentId: string, options?: AnyObj): string; }
39
+ interface AgreementFormatterI { format(agreements: unknown[] | null): string; }
40
+ interface LoggerI { info?(...args: unknown[]): void; warn?(...args: unknown[]): void; debug?(...args: unknown[]): void; }
41
+
42
+ export interface PromptBuilderOptions {
43
+ description?: string;
44
+ specialization?: string | null;
45
+ maxHistoryEntries?: number;
46
+ historyFormatter?: HistoryFormatterI;
47
+ agreementFormatter?: AgreementFormatterI;
48
+ logger?: LoggerI | null | { warn: (msg: string) => void };
49
+ }
50
+
51
+ /**
52
+ * Builds the XML prompt for a thinking-state turn:
53
+ * <agent><self/><world/><decision_schema/></agent>
54
+ *
55
+ * Reads Ctx (identity + persistent ids + lastOutcome) for <machine_context>.
56
+ * Action tool binding is direct (action.tool). Decision schema documents the
57
+ * action's `produces` outcome kind.
58
+ */
59
+ export class PromptBuilder {
60
+ description: string;
61
+ specialization: string | null;
62
+ maxHistoryEntries: number;
63
+ historyFormatter: HistoryFormatterI;
64
+ agreementFormatter: AgreementFormatterI;
65
+ logger: LoggerI | null;
66
+
67
+ constructor(options: PromptBuilderOptions = {}) {
68
+ this.description = options.description || 'A capable agent ready to assist';
69
+ this.specialization = options.specialization ?? null;
70
+ this.maxHistoryEntries = options.maxHistoryEntries ?? 50;
71
+ this.historyFormatter = options.historyFormatter || new HistoryFormatter();
72
+ this.agreementFormatter = (options.agreementFormatter || new AgreementFormatter()) as AgreementFormatterI;
73
+ this.logger = options.logger as LoggerI | null || null;
74
+ }
75
+
76
+ buildFromState(args: BuildArgs): string {
77
+ const { statePrompt, actions, serverContext, incomingEvent, definition, stateId, ctx } = args;
78
+ const ctxObj = serverContext?.context || {};
79
+ validateContext(ctxObj, { logger: this.logger as { warn: (msg: string) => void } | undefined });
80
+
81
+ const agentId = this._getMyAgentId(ctxObj);
82
+ const senderMeta = this._getSenderMeta(incomingEvent, ctxObj);
83
+ const agreements = ctxObj.agreements || [];
84
+ const otherAgents = (ctxObj.agents || []).filter((a) => !a.isYou);
85
+ const users = ctxObj.users || [];
86
+ const desc = definition?.description || this.description;
87
+
88
+ return `<agent>
89
+
90
+ ${this._renderSelfFromState(desc, statePrompt, serverContext?.tools)}
91
+
92
+ ${this._renderWorld(incomingEvent, ctxObj, agentId, senderMeta, agreements, otherAgents, users, stateId, ctx)}
93
+
94
+ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
95
+
96
+ </agent>`;
97
+ }
98
+
99
+ _renderSelfFromState(description: string, statePrompt: PromptDef, tools?: AnyObj[]): string {
100
+ const lines: string[] = ['<self>'];
101
+ lines.push(`<identity>\nYou are an Autonomous Agent.\n${description}\n</identity>`);
102
+
103
+ lines.push('\n<state>');
104
+ if (statePrompt.role) lines.push(`Role: ${statePrompt.role}`);
105
+ if (statePrompt.goal) lines.push(`Goal: ${statePrompt.goal}`);
106
+ if (statePrompt.context) lines.push(`Context: ${statePrompt.context}`);
107
+ if (statePrompt.constraints) lines.push(`Constraints: ${statePrompt.constraints}`);
108
+ lines.push('</state>');
109
+
110
+ if (tools?.length) {
111
+ lines.push('\n<tools>');
112
+ for (const tool of tools) {
113
+ const toolAny = tool as AnyObj;
114
+ const fn = (toolAny.schema as AnyObj | undefined)?.function as AnyObj | undefined || toolAny.schema as AnyObj | undefined;
115
+ const name = fn?.name;
116
+ const desc = fn?.description;
117
+ if (name) lines.push(`- ${name}${desc ? ': ' + desc : ''}`);
118
+ }
119
+ lines.push('</tools>');
120
+ }
121
+
122
+ lines.push('</self>');
123
+ return lines.join('\n');
124
+ }
125
+
126
+ _renderWorld(
127
+ event: RawEvent | null,
128
+ ctxObj: ServerContext,
129
+ agentId: string | null,
130
+ senderMeta: SenderMeta,
131
+ agreements: AnyObj[],
132
+ otherAgents: AnyObj[],
133
+ users: AnyObj[],
134
+ stateId: string,
135
+ ctx: Ctx,
136
+ ): string {
137
+ const lines: string[] = ['<world>'];
138
+
139
+ const machineBlock = this._renderMachineContext(stateId, ctx);
140
+ if (machineBlock) {
141
+ lines.push(machineBlock);
142
+ lines.push('');
143
+ }
144
+
145
+ lines.push(this._renderEvent(event, senderMeta));
146
+ lines.push('');
147
+ lines.push(this._renderHistory(ctxObj.history, agentId));
148
+ lines.push('');
149
+ lines.push(this._renderAgreements(agreements));
150
+
151
+ if (users.length > 0 || otherAgents.length > 0) {
152
+ lines.push('');
153
+ lines.push(this._renderRecipients(users, otherAgents, agreements, agentId));
154
+ }
155
+
156
+ lines.push(`\n<context>\n currentTime: ${new Date().toISOString()}\n</context>`);
157
+ lines.push('</world>');
158
+ return lines.join('\n');
159
+ }
160
+
161
+ _renderMachineContext(stateId: string, ctx: Ctx): string {
162
+ if (!ctx) return stateId ? `<machine_context>\n${JSON.stringify({ stateId }, null, 2)}\n</machine_context>` : '';
163
+ const out: Record<string, unknown> = { stateId };
164
+ if (ctx.identity?.sessionId) out.chatId = ctx.identity.sessionId;
165
+ if (ctx.identity?.laneKey) out.laneKey = ctx.identity.laneKey;
166
+ if (ctx.identity?.agentId) out.ownAgentId = ctx.identity.agentId;
167
+ if (ctx.activeAgreementId) out.activeAgreementId = ctx.activeAgreementId;
168
+ if (ctx.activeTaskId) out.activeTaskId = ctx.activeTaskId;
169
+ if (ctx.pendingProposalAgreementId) out.pendingProposalAgreementId = ctx.pendingProposalAgreementId;
170
+ if (ctx.delegatedAgreementIds?.length) out.delegatedAgreementIds = ctx.delegatedAgreementIds;
171
+ if (ctx.lastOutcome) out.lastOutcomeKind = ctx.lastOutcome.kind;
172
+ return `<machine_context>\n${JSON.stringify(out, null, 2)}\n</machine_context>`;
173
+ }
174
+
175
+ _renderEvent(event: RawEvent | AnyObj | null, senderMeta: SenderMeta): string {
176
+ if (!event) return '<event type="none">No incoming event.</event>';
177
+
178
+ const evAny = event as AnyObj;
179
+ if (evAny.type === 'batch' && Array.isArray(evAny.events) && (evAny.events as unknown[]).length > 0) {
180
+ const parts = (evAny.events as AnyObj[]).map((e: AnyObj, i: number) => {
181
+ const sub: SenderMeta = { ...senderMeta, senderId: (e.senderId ?? senderMeta.senderId) as string | undefined, senderType: (e.senderType ?? senderMeta.senderType) as string | undefined };
182
+ return (i > 0 ? '\n---\n' : '') + this._renderEvent(e, sub);
183
+ });
184
+ return `<event type="batch" count="${(evAny.events as unknown[]).length}">\n${parts.join('')}\n</event>`;
185
+ }
186
+
187
+ const eventType = String(evAny.type || 'message');
188
+ const from = `${senderMeta.senderId} (${String(senderMeta.senderType || 'unknown').toLowerCase()})`;
189
+
190
+ const result = evAny.result as AnyObj | undefined;
191
+ if (eventType === 'task_result' && result?.taskId) {
192
+ return this._renderTaskResultEvent(result, from);
193
+ }
194
+ if (eventType === 'tool_result') {
195
+ const r = typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result);
196
+ return `<event type="${eventType}" from="${from}">\nTool: ${evAny.tool}\nResult:\n${r}\n</event>`;
197
+ }
198
+ if (eventType === 'tool_error') {
199
+ return `<event type="${eventType}" from="${from}">\nTool Error: ${evAny.tool}\nMessage: ${evAny.error}\n</event>`;
200
+ }
201
+ if (eventType === 'message_sent') {
202
+ return `<event type="${eventType}" from="${from}">\nYour message was delivered. Do not repeat it.\n</event>`;
203
+ }
204
+
205
+ const content = event.text || event.result || event.content;
206
+ if (!content) return `<event type="${eventType}" from="${from}">(Empty)</event>`;
207
+ const str = typeof content === 'object' ? JSON.stringify(content) : String(content);
208
+ const display = str.length > 1000 ? str.slice(0, 1000) + '...' : str;
209
+ return `<event type="${eventType}" from="${from}">\n${display}\n</event>`;
210
+ }
211
+
212
+ _renderTaskResultEvent(task: AnyObj, from: string): string {
213
+ const lines: string[] = [];
214
+ const ag = task.agreement as AnyObj | undefined;
215
+ const parties = (ag?.parties as AnyObj) || {};
216
+ const creatorIsYou = task.creatorIsYou;
217
+ const providerIsYou = task.providerIsYou;
218
+
219
+ if (ag?.agreementId) {
220
+ lines.push(`Agreement: ${ag.agreementId}${creatorIsYou ? ' (yours)' : ''}`);
221
+ lines.push(` Creator: ${parties.creatorId ?? 'N/A'}${creatorIsYou ? ' (You)' : ''}`);
222
+ lines.push(` Provider: ${parties.providerId ?? 'N/A'}${providerIsYou ? ' (You)' : ''}`);
223
+ const proposal = ag.proposal as AnyObj | undefined;
224
+ if (proposal?.status) {
225
+ lines.push(` Proposal: ${proposal.status} (to ${parties.proposedToId}${task.proposedToIsYou ? ' - You' : ''})`);
226
+ }
227
+ const money = ag.money as AnyObj | undefined;
228
+ if (typeof money?.price === 'number' && (money.price as number) > 0) {
229
+ lines.push(` Price: ${money.price} cents (${money.paymentStatus})`);
230
+ }
231
+ }
232
+
233
+ lines.push('');
234
+ lines.push(`Task: ${task.taskId}`);
235
+ lines.push(` Description: ${task.description}`);
236
+ lines.push(` State: ${task.state}`);
237
+ if (task.parentTaskId) lines.push(` Parent task: ${task.parentTaskId}`);
238
+
239
+ // Tasks are always born active in the new model. Proposal lifecycle lives
240
+ // only on the agreement; the prompt surfaces it through the <agreements>
241
+ // section, not via task state.
242
+ void ag;
243
+ void parties;
244
+
245
+ return `<event type="task_result" from="${from}"${creatorIsYou ? ' own_agreement="true"' : ''}>\n${lines.join('\n')}\n</event>`;
246
+ }
247
+
248
+ _renderHistory(history: AnyObj[] | undefined, agentId: string | null): string {
249
+ let entries = history || [];
250
+ let truncNote = '';
251
+ if (entries.length > this.maxHistoryEntries) {
252
+ truncNote = `[Showing last ${this.maxHistoryEntries} of ${entries.length} entries]\n\n`;
253
+ entries = entries.slice(-this.maxHistoryEntries);
254
+ }
255
+ const formatted = this.historyFormatter.format(entries, agentId ?? '', {});
256
+ return `<history>\n${truncNote}${formatted}\n</history>`;
257
+ }
258
+
259
+ _renderAgreements(agreements: AnyObj[]): string {
260
+ const formatted = this.agreementFormatter.format(agreements || []);
261
+ return `<agreements>\n${formatted}\n</agreements>`;
262
+ }
263
+
264
+ _renderRecipients(users: AnyObj[], otherAgents: AnyObj[], agreements: AnyObj[], myAgentId: string | null | undefined): string {
265
+ // <recipients> is grouped by surface so the LLM knows which ids it can use
266
+ // for each kind of action. Same id appears in multiple subsections when it
267
+ // participates in multiple surfaces. <users> / <agents> stay as the typed
268
+ // directory (existing prompts reference <recipients><users>); the new
269
+ // <chat>/<agreement>/<task>/<transfer> subsections narrow that directory
270
+ // to a specific action surface.
271
+ const userIds = new Set<string>();
272
+ const userLabels = new Map<string, string>();
273
+ for (const u of users) {
274
+ const id = u?.userId as string | undefined;
275
+ if (!id || id === myAgentId) continue;
276
+ userIds.add(id);
277
+ userLabels.set(id, u.userName ? `${u.userName} (${id})` : id);
278
+ }
279
+ const agentIds = new Set<string>();
280
+ const agentLabels = new Map<string, string>();
281
+ for (const a of otherAgents) {
282
+ const id = a?.agentId as string | undefined;
283
+ if (!id || id === myAgentId) continue;
284
+ agentIds.add(id);
285
+ const desc = typeof a.description === 'string' ? `: ${a.description.slice(0, 80)}` : '';
286
+ const name = a.name && a.name !== id ? `${a.name} (${id})` : id;
287
+ agentLabels.set(id, `${name}${desc}`);
288
+ }
289
+ const kindOf = (id: string): 'user' | 'agent' | 'unknown' =>
290
+ userIds.has(id) ? 'user' : agentIds.has(id) ? 'agent' : 'unknown';
291
+ const labelOf = (id: string): string =>
292
+ userLabels.get(id) ?? agentLabels.get(id) ?? id;
293
+ const tagged = (id: string): string => `${labelOf(id)} [${kindOf(id)}]`;
294
+
295
+ // Agreement parties — derived from current agreements, role-tagged.
296
+ type PartyHit = { agreementId: string; role: string };
297
+ const agreementParties = new Map<string, PartyHit[]>();
298
+ for (const ag of agreements || []) {
299
+ const agId = ag?.agreementId as string | undefined;
300
+ const parties = (ag?.parties as AnyObj) || {};
301
+ const roles: Array<[string, string]> = [
302
+ ['creatorId', 'creator'],
303
+ ['providerId', 'provider'],
304
+ ['payerId', 'payer'],
305
+ ['proposedToId', 'proposedTo'],
306
+ ];
307
+ for (const [field, role] of roles) {
308
+ const pid = parties[field] as string | undefined;
309
+ if (!pid || pid === myAgentId) continue;
310
+ if (!agreementParties.has(pid)) agreementParties.set(pid, []);
311
+ agreementParties.get(pid)!.push({ agreementId: agId ?? '', role });
312
+ }
313
+ }
314
+
315
+ // Chat and transfer surfaces default to the full directory: every chat
316
+ // counterparty can receive a chat message; every id can receive a transfer
317
+ // by their principal id until per-wallet routing is wired through.
318
+ const chatIds = new Set<string>([...userIds, ...agentIds]);
319
+ const transferIds = new Set<string>([...userIds, ...agentIds]);
320
+
321
+ const lines: string[] = ['<recipients>'];
322
+
323
+ lines.push('<users>');
324
+ if (userIds.size === 0) lines.push(' (none)');
325
+ else for (const id of userIds) lines.push(` - ${userLabels.get(id)}`);
326
+ lines.push('</users>');
327
+
328
+ lines.push('<agents>');
329
+ if (agentIds.size === 0) lines.push(' (none)');
330
+ else for (const id of agentIds) lines.push(` - ${agentLabels.get(id)}`);
331
+ lines.push('</agents>');
332
+
333
+ lines.push('<chat>');
334
+ if (chatIds.size === 0) lines.push(' (none)');
335
+ else for (const id of chatIds) lines.push(` - ${tagged(id)}`);
336
+ lines.push('</chat>');
337
+
338
+ lines.push('<agreement>');
339
+ if (agreementParties.size === 0) lines.push(' (no agreement parties in this chat)');
340
+ else for (const [id, hits] of agreementParties) {
341
+ const tag = hits.map((h) => `${h.agreementId}:${h.role}`).join(', ');
342
+ lines.push(` - ${tagged(id)} [${tag}]`);
343
+ }
344
+ lines.push('</agreement>');
345
+
346
+ lines.push('<task>');
347
+ if (agreementParties.size === 0) lines.push(' (no task parties in this chat)');
348
+ else for (const [id, hits] of agreementParties) {
349
+ const tag = hits.map((h) => `${h.agreementId}:${h.role}`).join(', ');
350
+ lines.push(` - ${tagged(id)} [${tag}]`);
351
+ }
352
+ lines.push('</task>');
353
+
354
+ lines.push('<transfer>');
355
+ if (transferIds.size === 0) lines.push(' (none)');
356
+ else for (const id of transferIds) lines.push(` - ${tagged(id)}`);
357
+ lines.push('</transfer>');
358
+
359
+ lines.push('</recipients>');
360
+ return lines.join('\n');
361
+ }
362
+
363
+ _renderDecisionSchemaFromActions(actions: Record<string, Action>, senderMeta: SenderMeta, ctxObj: ServerContext): string {
364
+ const lines: string[] = ['<decision_schema>'];
365
+ lines.push('Return ONE JSON object. ALWAYS include "thought" as the FIRST field — briefly explain your reasoning before choosing an action.');
366
+ lines.push('Set "action" to exactly one of the action names below.');
367
+ 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');
368
+
369
+ const replyTo = (senderMeta.defaultReplyReceiverId as string) || '<receiverId>';
370
+ const pendingProposals = (ctxObj.agreements || []).filter(
371
+ (a) => (a.proposal as Record<string, unknown> | undefined)?.status === 'pending' && (a.proposedToIsYou || (a.parties as Record<string, unknown> | undefined)?.proposedToId === 'everyone'),
372
+ );
373
+
374
+ if (pendingProposals.length > 0) {
375
+ lines.push(`PRIORITY: You have ${pendingProposals.length} pending proposal(s) directed at you. Respond BEFORE other actions.`);
376
+ lines.push(`{"thought": "...", "action": "<action_bound_to_agreement_respond>", "args": {"agreementId": "<id>", "action": "approve|reject"}}\n`);
377
+ }
378
+
379
+ for (const [name, actionDef] of Object.entries(actions)) {
380
+ lines.push(`─── "${name}" ───`);
381
+ if (actionDef.prompt?.when) lines.push(`Use when: ${actionDef.prompt.when}`);
382
+ if (actionDef.prompt?.instruction) lines.push(`Instruction: ${actionDef.prompt.instruction}`);
383
+ if (actionDef.prompt?.format) lines.push(`Format: ${actionDef.prompt.format}`);
384
+
385
+ const boundTool = actionDef.tool ?? null;
386
+ if (boundTool) {
387
+ lines.push(`Tool: ${boundTool}`);
388
+ lines.push(`Response: {"thought": "...", "action": "${name}", "args": {...}}`);
389
+ } else {
390
+ lines.push(this._buildActionSchema(name, replyTo));
391
+ }
392
+
393
+ if (actionDef.prompt?.examples?.length) {
394
+ lines.push('Examples:');
395
+ for (const ex of actionDef.prompt.examples) {
396
+ const desc = ex.scenario || ex.input || '';
397
+ const out = ex.output || ex.query || '';
398
+ lines.push(` - ${desc}${out ? ' → ' + out : ''}`);
399
+ }
400
+ }
401
+
402
+ lines.push('');
403
+ }
404
+
405
+ lines.push('</decision_schema>');
406
+ return lines.join('\n');
407
+ }
408
+
409
+ _buildActionSchema(name: string, replyTo: string): string {
410
+ if (name === 'wait') {
411
+ return `Response: {"thought": "...", "action": "${name}"}`;
412
+ }
413
+ // Never pre-fill a system-owned id (ziggs-system, anything ending with
414
+ // `-system`, or the literal 'system'/'service' bucket) as the default
415
+ // receiverId — that just nudges the LLM to address chat replies to a
416
+ // non-routable destination. Fall back to an explicit placeholder so the
417
+ // model has to pick a real recipient from <recipients>.
418
+ const isSystemReplyTo =
419
+ !replyTo ||
420
+ replyTo === 'system' ||
421
+ replyTo === 'service' ||
422
+ replyTo.endsWith('-system') ||
423
+ replyTo.startsWith('<');
424
+ const renderedReplyTo = isSystemReplyTo ? '<receiverId from <recipients>>' : replyTo;
425
+ return `Response: {"thought": "...", "action": "${name}", "receiverId": "${renderedReplyTo}", "message": "..."}`;
426
+ }
427
+
428
+ _getSenderMeta(event: RawEvent | AnyObj | null, ctxObj: ServerContext): SenderMeta {
429
+ const ev = event as AnyObj | null;
430
+ const senderId = String(ev?.senderId || 'system');
431
+ const senderType = ev?.senderType ? String(ev.senderType).toUpperCase() : 'SYSTEM';
432
+ const myId = this._getMyAgentId(ctxObj);
433
+
434
+ const isSystemBucket = (id: string): boolean =>
435
+ !id || id === 'system' || id === 'service' || id.endsWith('-system');
436
+
437
+ let defaultReplyReceiverId = senderId;
438
+ const isNonRoutable = isSystemBucket(senderId) || senderType === 'SERVICE';
439
+ if (isNonRoutable) {
440
+ const history = ctxObj.history || [];
441
+ for (let i = history.length - 1; i >= 0; i--) {
442
+ const row = history[i];
443
+ const sender = row.sender as Record<string, unknown> | undefined;
444
+ const id = String(sender?.id || row.senderId || '');
445
+ const type = String(sender?.type || row.senderType || '').toUpperCase();
446
+ if (id && !isSystemBucket(id) && id !== myId && type !== 'SERVICE') {
447
+ defaultReplyReceiverId = id;
448
+ break;
449
+ }
450
+ }
451
+ }
452
+
453
+ return { senderId, senderType, myAgentId: myId, defaultReplyReceiverId: defaultReplyReceiverId || senderId };
454
+ }
455
+
456
+ _getMyAgentId(ctxObj: ServerContext): string | null {
457
+ const agents = ctxObj.agents || [];
458
+ const me = agents.find((a) => a.isYou);
459
+ return (me?.agentId as string | undefined) || null;
460
+ }
461
+ }