@ziggs-ai/agent-sdk 0.1.3 → 0.1.5

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