@ziggs-ai/agent-sdk 0.1.4 → 0.1.6
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/README.md +7 -5
- package/package.json +2 -2
- package/src/AgentHost.ts +165 -12
- package/src/adapters/OpenAIAdapter.ts +21 -0
- package/src/agent/Agent.ts +5 -2
- package/src/cognition/validateContext.ts +1 -1
- package/src/context/batch.ts +3 -3
- package/src/context/classifyEnvelope.ts +2 -2
- package/src/context/routingLabels.ts +1 -1
- package/src/formatters/AgreementFormatter.ts +5 -5
- package/src/formatters/HistoryFormatter.ts +3 -3
- package/src/index.ts +25 -4
- package/src/ingress/normalizeIncoming.ts +50 -7
- package/src/pricing/fleetDefaults.ts +218 -0
- package/src/pricing/fleetEvalFree.ts +24 -0
- package/src/pricing/fleetFreeTierA.gen.ts +12 -0
- package/src/pricing/fleetTierByAgentId.gen.ts +1022 -0
- package/src/runtime/AgentMachine.ts +68 -2
- package/src/runtime/PromptBuilder.ts +25 -23
- package/src/runtime/buildOutcome.ts +33 -3
- package/src/runtime/defaults.ts +3 -0
- package/src/runtime/runTurn.ts +115 -61
- package/src/runtime/validateWorkflow.ts +16 -0
- package/src/server/EventQueue.ts +14 -0
- package/src/server/InboxCatchUp.ts +251 -0
- package/src/server/SeenMessages.ts +27 -0
- package/src/server/ZiggsEffectHandler.ts +82 -8
- package/src/server/agreements/AgreementService.ts +7 -1
- package/src/server/createHealthServer.ts +79 -2
- package/src/server/runLauncher.ts +40 -25
- package/src/server/tasks/TaskService.ts +4 -5
- package/src/server/tasks/index.ts +0 -3
- package/src/server/telemetryIngest.ts +91 -0
- package/src/server/tools/index.ts +46 -0
- package/src/server/{tasks → tools/tier1}/protocolRunner.ts +52 -20
- package/src/server/{tasks → tools/tier1}/protocolTools.ts +6 -3
- package/src/server/tools/tier2/connectionTools.ts +75 -0
- package/src/server/tools/tier2/contextTools.ts +74 -0
- package/src/server/tools/tier2/discoveryTools.ts +34 -0
- package/src/server/tools/tier2/marketplaceTools.ts +25 -0
- package/src/server/{tasks → tools/tier2}/paymentTools.ts +74 -37
- package/src/server/ziggsconnect/ZiggsConnectClient.ts +126 -0
- package/src/server/ziggscontext/ZiggsContextClient.ts +137 -0
- package/src/server/ziggspay/ZiggsPayClient.ts +12 -12
- package/src/shared/types.ts +0 -2
- package/src/tools/index.ts +2 -0
- package/src/tools/recordReport.ts +82 -0
- package/src/types.ts +47 -8
- package/src/tasks/taskCore.ts +0 -139
|
@@ -12,6 +12,12 @@ export interface MachineSnapshot {
|
|
|
12
12
|
status: 'active' | 'stopped';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
export type TransitionEvent =
|
|
16
|
+
| { kind: 'transition'; fromState: string; toState: string; outcomeKind: string; depth: number }
|
|
17
|
+
| { kind: 'no-match'; state: string; outcomeKind: string; depth: number }
|
|
18
|
+
| { kind: 'depth-guard'; state: string; depth: number }
|
|
19
|
+
| { kind: 'error-escape'; fromState: string; toState: string };
|
|
20
|
+
|
|
15
21
|
interface MachineInput {
|
|
16
22
|
identity: Identity;
|
|
17
23
|
/** Server-provided ctx for an existing session. If omitted, a fresh ctx is built. */
|
|
@@ -31,6 +37,8 @@ interface MachineOptions {
|
|
|
31
37
|
/** Agent-private long-term memory. Operator-provided. */
|
|
32
38
|
memory?: MemoryStore;
|
|
33
39
|
input: MachineInput;
|
|
40
|
+
/** Optional tap for FSM transition events — called synchronously on each transition. */
|
|
41
|
+
transitionTap?: (event: TransitionEvent) => void;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
interface IncomingFrame {
|
|
@@ -69,17 +77,20 @@ export class AgentMachine {
|
|
|
69
77
|
_draining = false;
|
|
70
78
|
_llmCalls = 0;
|
|
71
79
|
_tokens = { total: 0, input: 0, output: 0 };
|
|
80
|
+
_lastUserSenderId: string | null = null;
|
|
81
|
+
_transitionTap: ((event: TransitionEvent) => void) | undefined;
|
|
72
82
|
|
|
73
83
|
get llmCalls(): number { return this._llmCalls; }
|
|
74
84
|
get tokens(): { total: number; input: number; output: number } { return this._tokens; }
|
|
75
85
|
|
|
76
|
-
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input }: MachineOptions) {
|
|
86
|
+
constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap }: MachineOptions) {
|
|
77
87
|
this.workflow = workflow;
|
|
78
88
|
this.executionCore = executionCore;
|
|
79
89
|
this.eff = eff;
|
|
80
90
|
this.toolManager = toolManager;
|
|
81
91
|
this.promptBuilder = promptBuilder;
|
|
82
92
|
this.memory = memory;
|
|
93
|
+
this._transitionTap = transitionTap;
|
|
83
94
|
this.state = input.state || workflow.initial;
|
|
84
95
|
this.ctx = input.ctx
|
|
85
96
|
? { ...input.ctx, identity: input.identity }
|
|
@@ -143,6 +154,9 @@ export class AgentMachine {
|
|
|
143
154
|
|
|
144
155
|
const ev = frame.event;
|
|
145
156
|
this._lastIncomingEvent = ev ?? null;
|
|
157
|
+
if (ev?.senderId && String(ev?.senderType ?? '').toUpperCase() === 'USER') {
|
|
158
|
+
this._lastUserSenderId = String(ev.senderId);
|
|
159
|
+
}
|
|
146
160
|
runtimeLog.debug(
|
|
147
161
|
'FSM',
|
|
148
162
|
`_handleFrame ev.type=${ev?.type} ev.senderId=${ev?.senderId} ev.receiverId=${ev?.receiverId} myId=${this.ctx.identity?.agentId}`,
|
|
@@ -180,6 +194,7 @@ export class AgentMachine {
|
|
|
180
194
|
`_step from=${fromState} outcome.kind=${o?.kind} senderId=${o?.senderId ?? '<none>'} myId=${this.ctx.identity?.agentId ?? '<none>'} → target=${target ?? '(stay)'} depth=${depth}`,
|
|
181
195
|
);
|
|
182
196
|
if (target && target !== this.state) {
|
|
197
|
+
this._transitionTap?.({ kind: 'transition', fromState, toState: target, outcomeKind: (outcome as Record<string, unknown>).kind as string, depth });
|
|
183
198
|
this.state = target;
|
|
184
199
|
this._notify();
|
|
185
200
|
}
|
|
@@ -193,6 +208,26 @@ export class AgentMachine {
|
|
|
193
208
|
return;
|
|
194
209
|
}
|
|
195
210
|
|
|
211
|
+
// A thinking turn that yields an outcome no transition handles must not
|
|
212
|
+
// silently re-enter the same state and burn another LLM call (ZIG-303).
|
|
213
|
+
// Explicit self-loops still match (target is non-null, even if target === state).
|
|
214
|
+
if (
|
|
215
|
+
sd.kind === 'thinking' &&
|
|
216
|
+
depth > 0 &&
|
|
217
|
+
fromState === this.state &&
|
|
218
|
+
!target
|
|
219
|
+
) {
|
|
220
|
+
if (this._escapeToInitialParked(outcome, fromState)) return;
|
|
221
|
+
runtimeLog.warn(
|
|
222
|
+
'AgentMachine',
|
|
223
|
+
`state=${fromState} outcome.kind=${(outcome as Record<string, unknown>).kind} matched no transition; parking (depth=${depth})`,
|
|
224
|
+
);
|
|
225
|
+
this._transitionTap?.({ kind: 'no-match', state: fromState, outcomeKind: (outcome as Record<string, unknown>).kind as string, depth });
|
|
226
|
+
await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
|
|
227
|
+
this._notify();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
196
231
|
// Recursion / runaway-turn guard. Without this, a thinking state whose
|
|
197
232
|
// transitions don't match the turn outcome (e.g. `delegating` not
|
|
198
233
|
// catching `tool-result` from an agent_network search) re-enters the
|
|
@@ -207,6 +242,8 @@ export class AgentMachine {
|
|
|
207
242
|
'AgentMachine',
|
|
208
243
|
`state=${this.state} runaway turn loop (depth=${depth}); parking`,
|
|
209
244
|
);
|
|
245
|
+
this._transitionTap?.({ kind: 'depth-guard', state: this.state, depth });
|
|
246
|
+
await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
|
|
210
247
|
this._notify();
|
|
211
248
|
return;
|
|
212
249
|
}
|
|
@@ -219,7 +256,7 @@ export class AgentMachine {
|
|
|
219
256
|
stateId: this.state,
|
|
220
257
|
statePrompt: sd.prompt,
|
|
221
258
|
actions: sd.actions,
|
|
222
|
-
incomingEvent: this.
|
|
259
|
+
incomingEvent: this._lastIncomingEvent,
|
|
223
260
|
ctx: this.ctx,
|
|
224
261
|
eff: this.eff,
|
|
225
262
|
toolManager: this.toolManager,
|
|
@@ -255,10 +292,39 @@ export class AgentMachine {
|
|
|
255
292
|
if (fallback && fallback !== this.state) {
|
|
256
293
|
this.state = fallback;
|
|
257
294
|
this._notify();
|
|
295
|
+
} else {
|
|
296
|
+
await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
|
|
258
297
|
}
|
|
259
298
|
}
|
|
260
299
|
}
|
|
261
300
|
|
|
301
|
+
/** Error with no author-defined escape: park at workflow.initial if it is parked. */
|
|
302
|
+
_escapeToInitialParked(outcome: Outcome, fromState: string): boolean {
|
|
303
|
+
if (outcome.kind !== 'error') return false;
|
|
304
|
+
const initial = this.workflow.initial;
|
|
305
|
+
const initialSd = this.workflow.states[initial];
|
|
306
|
+
if (!initialSd || initialSd.kind !== 'parked' || initial === fromState) {
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
runtimeLog.warn(
|
|
310
|
+
'AgentMachine',
|
|
311
|
+
`state=${fromState} error outcome had no transition — escaped to parked initial "${initial}"`,
|
|
312
|
+
);
|
|
313
|
+
this._transitionTap?.({ kind: 'error-escape', fromState, toState: initial });
|
|
314
|
+
this.state = initial;
|
|
315
|
+
this._notify();
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** Sends a short fallback to the last known human user when the machine parks unexpectedly. Best-effort — never throws. */
|
|
320
|
+
async _sendFallbackIfUserWaiting(text: string): Promise<void> {
|
|
321
|
+
const userId = this._lastUserSenderId;
|
|
322
|
+
if (!userId) return;
|
|
323
|
+
try {
|
|
324
|
+
await this.eff({ kind: 'send-message', sessionId: this.ctx.identity.sessionId, receiverId: userId, text });
|
|
325
|
+
} catch { /* best-effort */ }
|
|
326
|
+
}
|
|
327
|
+
|
|
262
328
|
_evaluateTransitions(outcome: Outcome): string | null {
|
|
263
329
|
const sd = this.workflow.states[this.state];
|
|
264
330
|
if (!sd?.transitions?.length) {
|
|
@@ -33,6 +33,8 @@ interface BuildArgs {
|
|
|
33
33
|
chatId: string | null;
|
|
34
34
|
stateId: string;
|
|
35
35
|
ctx: Ctx;
|
|
36
|
+
/** When true, respond-action schemas omit the "message" field — used for 2-call streaming (ZIG-471). */
|
|
37
|
+
omitMessageFromRespond?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
interface HistoryFormatterI { format(history: unknown[] | null | undefined, agentId: string, options?: AnyObj): string; }
|
|
@@ -74,7 +76,7 @@ export class PromptBuilder {
|
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
buildFromState(args: BuildArgs): string {
|
|
77
|
-
const { statePrompt, actions, serverContext, incomingEvent, definition, stateId, ctx } = args;
|
|
79
|
+
const { statePrompt, actions, serverContext, incomingEvent, definition, stateId, ctx, omitMessageFromRespond } = args;
|
|
78
80
|
const ctxObj = serverContext?.context || {};
|
|
79
81
|
validateContext(ctxObj, { logger: this.logger as { warn: (msg: string) => void } | undefined });
|
|
80
82
|
|
|
@@ -91,7 +93,7 @@ ${this._renderSelfFromState(desc, statePrompt, serverContext?.tools)}
|
|
|
91
93
|
|
|
92
94
|
${this._renderWorld(incomingEvent, ctxObj, agentId, senderMeta, agreements, otherAgents, users, stateId, ctx)}
|
|
93
95
|
|
|
94
|
-
${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
96
|
+
${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj, omitMessageFromRespond)}
|
|
95
97
|
|
|
96
98
|
</agent>`;
|
|
97
99
|
}
|
|
@@ -218,11 +220,11 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
218
220
|
|
|
219
221
|
if (ag?.agreementId) {
|
|
220
222
|
lines.push(`Agreement: ${ag.agreementId}${creatorIsYou ? ' (yours)' : ''}`);
|
|
221
|
-
lines.push(` Creator: ${parties.
|
|
222
|
-
lines.push(` Provider: ${parties.
|
|
223
|
+
lines.push(` Creator: ${parties.creator ?? 'N/A'}${creatorIsYou ? ' (You)' : ''}`);
|
|
224
|
+
lines.push(` Provider: ${parties.provider ?? 'N/A'}${providerIsYou ? ' (You)' : ''}`);
|
|
223
225
|
const proposal = ag.proposal as AnyObj | undefined;
|
|
224
226
|
if (proposal?.status) {
|
|
225
|
-
lines.push(` Proposal: ${proposal.status} (to ${parties.
|
|
227
|
+
lines.push(` Proposal: ${proposal.status} (to ${parties.proposedTo}${task.proposedToIsYou ? ' - You' : ''})`);
|
|
226
228
|
}
|
|
227
229
|
const money = ag.money as AnyObj | undefined;
|
|
228
230
|
if (typeof money?.price === 'number' && (money.price as number) > 0) {
|
|
@@ -299,10 +301,10 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
299
301
|
const agId = ag?.agreementId as string | undefined;
|
|
300
302
|
const parties = (ag?.parties as AnyObj) || {};
|
|
301
303
|
const roles: Array<[string, string]> = [
|
|
302
|
-
['
|
|
303
|
-
['
|
|
304
|
-
['
|
|
305
|
-
['
|
|
304
|
+
['creator', 'creator'],
|
|
305
|
+
['provider', 'provider'],
|
|
306
|
+
['payer', 'payer'],
|
|
307
|
+
['proposedTo', 'proposedTo'],
|
|
306
308
|
];
|
|
307
309
|
for (const [field, role] of roles) {
|
|
308
310
|
const pid = parties[field] as string | undefined;
|
|
@@ -360,7 +362,7 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
360
362
|
return lines.join('\n');
|
|
361
363
|
}
|
|
362
364
|
|
|
363
|
-
_renderDecisionSchemaFromActions(actions: Record<string, Action>, senderMeta: SenderMeta, ctxObj: ServerContext): string {
|
|
365
|
+
_renderDecisionSchemaFromActions(actions: Record<string, Action>, senderMeta: SenderMeta, ctxObj: ServerContext, omitMessage?: boolean): string {
|
|
364
366
|
const lines: string[] = ['<decision_schema>'];
|
|
365
367
|
lines.push('Return ONE JSON object. ALWAYS include "thought" as the FIRST field — briefly explain your reasoning before choosing an action.');
|
|
366
368
|
lines.push('Set "action" to exactly one of the action names below.');
|
|
@@ -368,7 +370,7 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
368
370
|
|
|
369
371
|
const replyTo = (senderMeta.defaultReplyReceiverId as string) || '<receiverId>';
|
|
370
372
|
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)?.
|
|
373
|
+
(a) => (a.proposal as Record<string, unknown> | undefined)?.status === 'pending' && (a.proposedToIsYou || (a.parties as Record<string, unknown> | undefined)?.proposedTo === 'everyone'),
|
|
372
374
|
);
|
|
373
375
|
|
|
374
376
|
if (pendingProposals.length > 0) {
|
|
@@ -387,7 +389,7 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
387
389
|
lines.push(`Tool: ${boundTool}`);
|
|
388
390
|
lines.push(`Response: {"thought": "...", "action": "${name}", "args": {...}}`);
|
|
389
391
|
} else {
|
|
390
|
-
lines.push(this._buildActionSchema(name, replyTo));
|
|
392
|
+
lines.push(this._buildActionSchema(name, replyTo, omitMessage));
|
|
391
393
|
}
|
|
392
394
|
|
|
393
395
|
if (actionDef.prompt?.examples?.length) {
|
|
@@ -406,22 +408,22 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
406
408
|
return lines.join('\n');
|
|
407
409
|
}
|
|
408
410
|
|
|
409
|
-
_buildActionSchema(name: string, replyTo: string): string {
|
|
411
|
+
_buildActionSchema(name: string, replyTo: string, omitMessage?: boolean): string {
|
|
410
412
|
if (name === 'wait') {
|
|
411
413
|
return `Response: {"thought": "...", "action": "${name}"}`;
|
|
412
414
|
}
|
|
413
|
-
// Never pre-fill a
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
// non-routable destination. Fall back to an explicit placeholder so the
|
|
417
|
-
// model has to pick a real recipient from <recipients>.
|
|
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
418
|
const isSystemReplyTo =
|
|
419
419
|
!replyTo ||
|
|
420
420
|
replyTo === 'system' ||
|
|
421
421
|
replyTo === 'service' ||
|
|
422
|
-
replyTo.endsWith('-system') ||
|
|
423
422
|
replyTo.startsWith('<');
|
|
424
423
|
const renderedReplyTo = isSystemReplyTo ? '<receiverId from <recipients>>' : replyTo;
|
|
424
|
+
if (omitMessage) {
|
|
425
|
+
return `Response: {"thought": "...", "action": "${name}", "receiverId": "${renderedReplyTo}"}`;
|
|
426
|
+
}
|
|
425
427
|
return `Response: {"thought": "...", "action": "${name}", "receiverId": "${renderedReplyTo}", "message": "..."}`;
|
|
426
428
|
}
|
|
427
429
|
|
|
@@ -431,11 +433,11 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
431
433
|
const senderType = ev?.senderType ? String(ev.senderType).toUpperCase() : 'SYSTEM';
|
|
432
434
|
const myId = this._getMyAgentId(ctxObj);
|
|
433
435
|
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
+
const isNonRoutableSenderType = (t: string): boolean =>
|
|
437
|
+
t === 'SYSTEM' || t === 'SERVICE';
|
|
436
438
|
|
|
437
439
|
let defaultReplyReceiverId = senderId;
|
|
438
|
-
const isNonRoutable =
|
|
440
|
+
const isNonRoutable = isNonRoutableSenderType(senderType);
|
|
439
441
|
if (isNonRoutable) {
|
|
440
442
|
const history = ctxObj.history || [];
|
|
441
443
|
for (let i = history.length - 1; i >= 0; i--) {
|
|
@@ -443,7 +445,7 @@ ${this._renderDecisionSchemaFromActions(actions, senderMeta, ctxObj)}
|
|
|
443
445
|
const sender = row.sender as Record<string, unknown> | undefined;
|
|
444
446
|
const id = String(sender?.id || row.senderId || '');
|
|
445
447
|
const type = String(sender?.type || row.senderType || '').toUpperCase();
|
|
446
|
-
if (id &&
|
|
448
|
+
if (id && id !== myId && !isNonRoutableSenderType(type)) {
|
|
447
449
|
defaultReplyReceiverId = id;
|
|
448
450
|
break;
|
|
449
451
|
}
|
|
@@ -83,6 +83,25 @@ export function outcomeFromEvent(
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
function singleEventToOutcome(ev: RawEvent, ownAgentId: string | null): Outcome | null {
|
|
86
|
+
if (ev.type === 'agreement_lifecycle') {
|
|
87
|
+
const operation = ev.operation as string | undefined;
|
|
88
|
+
const agreementId = (ev.agreementId as string | undefined) ?? '';
|
|
89
|
+
const action =
|
|
90
|
+
operation === 'proposal_rejected'
|
|
91
|
+
? 'reject'
|
|
92
|
+
: operation === 'proposal_approved' || operation === 'proposal_approved_execute'
|
|
93
|
+
? 'approve'
|
|
94
|
+
: null;
|
|
95
|
+
if (action && agreementId) {
|
|
96
|
+
return {
|
|
97
|
+
kind: 'proposal-resolved',
|
|
98
|
+
action,
|
|
99
|
+
proposal: { agreementId },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
86
105
|
if (ev.type === 'task_result') {
|
|
87
106
|
const result = ev.result || {};
|
|
88
107
|
if (
|
|
@@ -120,7 +139,7 @@ function singleEventToOutcome(ev: RawEvent, ownAgentId: string | null): Outcome
|
|
|
120
139
|
|
|
121
140
|
// Direct assignment to me (provider) — task spawned, citing an active agreement.
|
|
122
141
|
const isAssignedToMe =
|
|
123
|
-
(result.providerIsYou || parties.
|
|
142
|
+
(result.providerIsYou || parties.provider === ownAgentId) &&
|
|
124
143
|
(state === 'active' || state === 'in-progress');
|
|
125
144
|
if (isAssignedToMe) {
|
|
126
145
|
return { kind: 'task-assigned', task };
|
|
@@ -193,7 +212,8 @@ function singleEventToOutcome(ev: RawEvent, ownAgentId: string | null): Outcome
|
|
|
193
212
|
if (ownAgentId && ev.senderId && ev.senderId === ownAgentId) return null;
|
|
194
213
|
|
|
195
214
|
const text = typeof ev.text === 'string' ? ev.text : undefined;
|
|
196
|
-
|
|
215
|
+
const senderType = typeof ev.senderType === 'string' ? ev.senderType : undefined;
|
|
216
|
+
return { kind: 'message-received', text, senderId: ev.senderId, senderType };
|
|
197
217
|
}
|
|
198
218
|
|
|
199
219
|
// Higher rank wins when collapsing batched events into one outcome.
|
|
@@ -466,7 +486,7 @@ export function applyOutcomeToCtx(ctx: Ctx, outcome: Outcome): void {
|
|
|
466
486
|
// activeAgreementId so transitions gated on activeAgreementId fire.
|
|
467
487
|
// We only do this when no agreement is currently active, so that
|
|
468
488
|
// approvals of CHILD sub-agreements (e.g. coffee-agent approving the
|
|
469
|
-
// Ziggs-issued subcontract) don't overwrite the root the
|
|
489
|
+
// Ziggs-issued subcontract) don't overwrite the root the primary agent
|
|
470
490
|
// is bound to. Sub-agreement bookkeeping lives in delegatedAgreementIds
|
|
471
491
|
// (recorded on task-delegated) — not here.
|
|
472
492
|
if (outcome.action === 'approve' && !ctx.activeAgreementId) {
|
|
@@ -476,10 +496,20 @@ export function applyOutcomeToCtx(ctx: Ctx, outcome: Outcome): void {
|
|
|
476
496
|
ctx.pendingProposalAgreementId = null;
|
|
477
497
|
return;
|
|
478
498
|
}
|
|
499
|
+
case 'subtask-finished': {
|
|
500
|
+
const t = outcome.task;
|
|
501
|
+
const taskId = t?.taskId;
|
|
502
|
+
const agId = t?.agreementId || t?.agreement?.agreementId;
|
|
503
|
+
if (taskId) ctx.delegatedTaskIds = ctx.delegatedTaskIds.filter(id => id !== taskId);
|
|
504
|
+
if (agId) ctx.delegatedAgreementIds = ctx.delegatedAgreementIds.filter(id => id !== agId);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
479
507
|
case 'task-closed': {
|
|
480
508
|
ctx.activeAgreementId = null;
|
|
481
509
|
ctx.activeTaskId = null;
|
|
482
510
|
ctx.pendingProposalAgreementId = null;
|
|
511
|
+
ctx.delegatedTaskIds = [];
|
|
512
|
+
ctx.delegatedAgreementIds = [];
|
|
483
513
|
return;
|
|
484
514
|
}
|
|
485
515
|
default:
|
package/src/runtime/defaults.ts
CHANGED
|
@@ -40,6 +40,9 @@ export function thinkingDefaults(opts: { initial: string }): ThinkingDefaults {
|
|
|
40
40
|
transitions: [
|
|
41
41
|
{ to: initial, when: o => o.kind === 'wait' },
|
|
42
42
|
{ to: initial, when: o => o.kind === 'message-sent' },
|
|
43
|
+
// Errors (empty/failed turns) fall back to the initial state instead of
|
|
44
|
+
// silently re-entering and recursing until the depth guard (ZIG-303).
|
|
45
|
+
{ to: initial, when: o => o.kind === 'error' },
|
|
43
46
|
],
|
|
44
47
|
};
|
|
45
48
|
}
|
package/src/runtime/runTurn.ts
CHANGED
|
@@ -68,6 +68,8 @@ interface RunTurnInput {
|
|
|
68
68
|
*/
|
|
69
69
|
memory?: MemoryStore;
|
|
70
70
|
definition: { description?: string };
|
|
71
|
+
/** When true, respond actions use 2-call streaming: Call 1 picks action+receiverId, Call 2 streams the text (ZIG-471). */
|
|
72
|
+
streamRespondActions?: boolean;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
interface RunTurnResult {
|
|
@@ -95,7 +97,17 @@ interface RunTurnResult {
|
|
|
95
97
|
*/
|
|
96
98
|
export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
97
99
|
const { stateId, statePrompt, actions, incomingEvent, ctx, eff, toolManager, promptBuilder, memory, definition } = input;
|
|
100
|
+
const streamRespondActions = input.streamRespondActions ?? (process.env.ENABLE_LLM_STREAMING === 'true');
|
|
98
101
|
const sessionId = ctx.identity.sessionId;
|
|
102
|
+
const runId = crypto.randomUUID();
|
|
103
|
+
|
|
104
|
+
// Wrap eff to stamp runId + stateId onto llm-call and tool-call effects.
|
|
105
|
+
const effWithTrace: typeof eff = (effect) => {
|
|
106
|
+
if (effect.kind === 'llm-call' || effect.kind === 'tool-call') {
|
|
107
|
+
return eff({ ...effect, runId, stateId } as typeof effect);
|
|
108
|
+
}
|
|
109
|
+
return eff(effect);
|
|
110
|
+
};
|
|
99
111
|
|
|
100
112
|
const emitThought = async (text: unknown): Promise<void> => {
|
|
101
113
|
if (text == null) return;
|
|
@@ -108,7 +120,7 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
|
108
120
|
}
|
|
109
121
|
};
|
|
110
122
|
|
|
111
|
-
runtimeLog.info('runTurn', `enter state=${stateId}`);
|
|
123
|
+
runtimeLog.info('runTurn', `enter state=${stateId} runId=${runId}`);
|
|
112
124
|
|
|
113
125
|
// Parallel: read session context + collect tool schemas (pure).
|
|
114
126
|
// Filter tools to only those bound to an action in the current state — the
|
|
@@ -128,32 +140,15 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
|
128
140
|
return n != null && allowedToolNames.has(n);
|
|
129
141
|
})
|
|
130
142
|
: [];
|
|
131
|
-
const serverContext = await
|
|
143
|
+
const serverContext = await effWithTrace({ kind: 'read-context', sessionId, opts: {} });
|
|
132
144
|
const ctxObj = normalizeContextSnapshot(serverContext);
|
|
133
145
|
|
|
134
|
-
const probeDispatch = Object.entries(actions).find(([, a]) => a.tool === 'capability_probe_dispatch');
|
|
135
|
-
if (probeDispatch && incomingEvent) {
|
|
136
|
-
const [actionName, actionDef] = probeDispatch;
|
|
137
|
-
const messageText =
|
|
138
|
-
extractIncomingMessageText(incomingEvent, ctxObj as ContextSnapshot) ||
|
|
139
|
-
JSON.stringify(incomingEvent);
|
|
140
|
-
const toolResultsOut: ToolResultEntry[] = [];
|
|
141
|
-
const emitted = await executeTool(
|
|
142
|
-
{ action: actionName, tool: 'capability_probe_dispatch', args: { messageText } },
|
|
143
|
-
eff,
|
|
144
|
-
sessionId,
|
|
145
|
-
memory,
|
|
146
|
-
);
|
|
147
|
-
const events = (Array.isArray(emitted) ? emitted : emitted ? [emitted] : []) as EmittedEvent[];
|
|
148
|
-
const outcome = outcomeFromActionResult(actionName, actionDef, {}, events, toolResultsOut);
|
|
149
|
-
return { outcome, toolResults: toolResultsOut, llmCalls: 0, tokens: { total: 0, input: 0, output: 0 } };
|
|
150
|
-
}
|
|
151
|
-
|
|
152
146
|
const prompt = promptBuilder.buildFromState({
|
|
153
147
|
statePrompt, actions,
|
|
154
148
|
serverContext: { context: ctxObj as Record<string, unknown>, tools: tools as unknown as Record<string, unknown>[] },
|
|
155
149
|
incomingEvent, definition,
|
|
156
150
|
chatId: sessionId, stateId, ctx,
|
|
151
|
+
omitMessageFromRespond: streamRespondActions,
|
|
157
152
|
});
|
|
158
153
|
|
|
159
154
|
const systemContent = tools.length > 0
|
|
@@ -179,7 +174,12 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
|
179
174
|
runtimeLog.info('runTurn', `calling LLM tools=${tools.length}`);
|
|
180
175
|
|
|
181
176
|
for (let iter = 0; iter < MAX_TOOL_LOOP_ITERS; iter++) {
|
|
182
|
-
const llmResponse: LlmResponse = await
|
|
177
|
+
const llmResponse: LlmResponse = await effWithTrace({
|
|
178
|
+
kind: 'llm-call',
|
|
179
|
+
messages,
|
|
180
|
+
tools: tools as unknown as LlmToolSchema[],
|
|
181
|
+
sessionId,
|
|
182
|
+
});
|
|
183
183
|
llmCallsTotal++;
|
|
184
184
|
const usage = llmResponse?.usage;
|
|
185
185
|
tokensTotal += (usage?.total_tokens ?? usage?.totalTokens) || 0;
|
|
@@ -257,7 +257,7 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
|
257
257
|
try { args = JSON.parse(tc.function.arguments || '{}'); } catch {}
|
|
258
258
|
const decision = { action: matchingAction, tool: toolName, args };
|
|
259
259
|
|
|
260
|
-
const result = await executeDecision(decision, actionDef,
|
|
260
|
+
const result = await executeDecision(decision, actionDef, effWithTrace, sessionId, memory);
|
|
261
261
|
toolCallResults.push({ tc, matchingAction, result, args, actionDef });
|
|
262
262
|
}
|
|
263
263
|
|
|
@@ -286,7 +286,7 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
|
286
286
|
if (!turnHadAgreementCreation && eventsContainAgreementCreation(events)) {
|
|
287
287
|
turnHadAgreementCreation = true;
|
|
288
288
|
}
|
|
289
|
-
if (eventsAreTerminal(events)) hasTerminal = true;
|
|
289
|
+
if (eventsAreTerminal(events) || actionDef?.terminatesLoop) hasTerminal = true;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
if (hasTerminal) break;
|
|
@@ -301,6 +301,42 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
|
301
301
|
|
|
302
302
|
if (decision.thought) await emitThought(decision.thought);
|
|
303
303
|
|
|
304
|
+
// 2-call streaming: if this is a respond action (has receiverId, no bound tool),
|
|
305
|
+
// use the streaming effect to generate text and send chunks to the client.
|
|
306
|
+
runtimeLog.info('runTurn', `streaming check: enabled=${streamRespondActions} receiverId=${decision.receiverId ?? 'none'} tool=${actionDef?.tool ?? 'none'} action=${chosenAction}`);
|
|
307
|
+
if (streamRespondActions && decision.receiverId && !actionDef?.tool && chosenAction !== 'wait') {
|
|
308
|
+
const streamMessageId = crypto.randomUUID();
|
|
309
|
+
runtimeLog.info('runTurn', `stream-text start action=${chosenAction} receiverId=${decision.receiverId} msgId=${streamMessageId}`);
|
|
310
|
+
const streamMessages: LlmMessage[] = [
|
|
311
|
+
...messages,
|
|
312
|
+
{
|
|
313
|
+
role: 'assistant' as const,
|
|
314
|
+
content: JSON.stringify({ thought: decision.thought, action: decision.action, receiverId: decision.receiverId }),
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
role: 'user' as const,
|
|
318
|
+
content: 'Write your reply now. Output ONLY the message text — no JSON wrapping, no preamble.',
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
let streamedText = '';
|
|
322
|
+
try {
|
|
323
|
+
const streamResult = await eff({ kind: 'stream-text', sessionId, receiverId: decision.receiverId, messageId: streamMessageId, messages: streamMessages });
|
|
324
|
+
streamedText = streamResult.text ?? '';
|
|
325
|
+
runtimeLog.info('runTurn', `stream-text done chars=${streamedText.length}`);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
runtimeLog.warn('runTurn', `stream-text failed: ${(err as Error).message}`);
|
|
328
|
+
}
|
|
329
|
+
if (streamedText) {
|
|
330
|
+
const safeText = truncateMsg(streamedText);
|
|
331
|
+
await eff({ kind: 'send-message', sessionId, receiverId: decision.receiverId, text: safeText, messageId: streamMessageId });
|
|
332
|
+
const events: EmittedEvent[] = [{ type: 'message_sent', receiverId: decision.receiverId, chatId: sessionId, message: safeText, senderId: 'system' }];
|
|
333
|
+
allEmittedEvents.push(...events);
|
|
334
|
+
lastAction = chosenAction;
|
|
335
|
+
lastActionDef = actionDef;
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
|
|
304
340
|
const result = await executeDecision(decision, actionDef, eff, sessionId, memory);
|
|
305
341
|
if (result) {
|
|
306
342
|
const events = (Array.isArray(result) ? result : [result]) as EmittedEvent[];
|
|
@@ -340,6 +376,25 @@ export async function runTurn(input: RunTurnInput): Promise<RunTurnResult> {
|
|
|
340
376
|
|
|
341
377
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
342
378
|
|
|
379
|
+
function extractIncomingMessageText(event: RawEvent | null): string {
|
|
380
|
+
if (!event) return '';
|
|
381
|
+
const parts: string[] = [];
|
|
382
|
+
const push = (v: unknown) => {
|
|
383
|
+
if (typeof v === 'string' && v.trim()) parts.push(v.trim());
|
|
384
|
+
};
|
|
385
|
+
push(event.text);
|
|
386
|
+
push((event as Record<string, unknown>).message);
|
|
387
|
+
const agreement = (event as Record<string, unknown>).agreement as Record<string, unknown> | undefined;
|
|
388
|
+
if (agreement) {
|
|
389
|
+
const terms = agreement.terms as Record<string, unknown> | undefined;
|
|
390
|
+
push(terms?.description);
|
|
391
|
+
push(JSON.stringify(agreement));
|
|
392
|
+
}
|
|
393
|
+
const task = (event as Record<string, unknown>).task;
|
|
394
|
+
if (task) push(JSON.stringify(task));
|
|
395
|
+
return parts.join('\n');
|
|
396
|
+
}
|
|
397
|
+
|
|
343
398
|
function normalizeContextSnapshot(raw: ContextSnapshot | undefined | null): ContextSnapshot {
|
|
344
399
|
if (!raw || typeof raw !== 'object') {
|
|
345
400
|
return { history: [], agreements: [], agents: [], users: [] };
|
|
@@ -460,21 +515,38 @@ function findActionForTool(toolName: string, allowedActionNames: string[], actio
|
|
|
460
515
|
return null;
|
|
461
516
|
}
|
|
462
517
|
|
|
463
|
-
async function executeDecision(
|
|
518
|
+
async function executeDecision(
|
|
519
|
+
decision: ParsedDecision,
|
|
520
|
+
actionDef: Action | null,
|
|
521
|
+
eff: EffectHandler,
|
|
522
|
+
sessionId: string,
|
|
523
|
+
memory?: MemoryStore,
|
|
524
|
+
incomingEvent: RawEvent | null = null,
|
|
525
|
+
): Promise<EmittedEvent[] | EmittedEvent | null> {
|
|
464
526
|
const boundTool = actionDef?.tool || null;
|
|
465
527
|
|
|
466
528
|
if (boundTool) {
|
|
467
529
|
decision.tool = boundTool;
|
|
468
|
-
return await executeTool(decision, eff, sessionId, memory);
|
|
530
|
+
return await executeTool(decision, eff, sessionId, memory, incomingEvent);
|
|
469
531
|
}
|
|
470
|
-
if (decision.action === '_genericDiscoveryTool' && decision.tool)
|
|
471
|
-
|
|
532
|
+
if (decision.action === '_genericDiscoveryTool' && decision.tool) {
|
|
533
|
+
return await executeTool(decision, eff, sessionId, memory, incomingEvent);
|
|
534
|
+
}
|
|
535
|
+
if (decision.tool) return await executeTool(decision, eff, sessionId, memory, incomingEvent);
|
|
472
536
|
if (decision.messages && Array.isArray(decision.messages) && decision.messages.length > 0) {
|
|
473
537
|
return await executeSendMessages(decision, eff, sessionId);
|
|
474
538
|
}
|
|
475
539
|
if (decision.message && decision.receiverId) {
|
|
476
540
|
return await executeSendMessage(decision, eff, sessionId);
|
|
477
541
|
}
|
|
542
|
+
// Fallback: LLM may place message/receiverId inside args due to format ambiguity
|
|
543
|
+
// between prompt.format ("args: { receiverId, message }") and the top-level schema.
|
|
544
|
+
if (!decision.message && !decision.receiverId && decision.args) {
|
|
545
|
+
const a = decision.args as Record<string, unknown>;
|
|
546
|
+
if (typeof a.message === 'string' && typeof a.receiverId === 'string') {
|
|
547
|
+
return await executeSendMessage({ ...decision, message: a.message, receiverId: a.receiverId }, eff, sessionId);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
478
550
|
if (decision.action === 'wait') return { type: 'waited' };
|
|
479
551
|
return null;
|
|
480
552
|
}
|
|
@@ -522,11 +594,25 @@ async function executeSendMessages(decision: ParsedDecision, eff: EffectHandler,
|
|
|
522
594
|
* dependencies (taskService, agreementService, agents list, etc.), and
|
|
523
595
|
* wrapping the call in telemetry / operation-record entries if it wants.
|
|
524
596
|
*/
|
|
525
|
-
async function executeTool(
|
|
597
|
+
async function executeTool(
|
|
598
|
+
decision: ParsedDecision,
|
|
599
|
+
eff: EffectHandler,
|
|
600
|
+
sessionId: string,
|
|
601
|
+
memory?: MemoryStore,
|
|
602
|
+
incomingEvent: RawEvent | null = null,
|
|
603
|
+
): Promise<EmittedEvent[] | EmittedEvent | null> {
|
|
526
604
|
const { tool, args } = decision;
|
|
527
605
|
if (!tool) return null;
|
|
528
606
|
try {
|
|
529
|
-
const r = await eff({
|
|
607
|
+
const r = await eff({
|
|
608
|
+
kind: 'tool-call',
|
|
609
|
+
sessionId,
|
|
610
|
+
name: tool,
|
|
611
|
+
args: args || {},
|
|
612
|
+
memory,
|
|
613
|
+
senderId: incomingEvent?.senderId != null ? String(incomingEvent.senderId) : undefined,
|
|
614
|
+
senderType: incomingEvent?.senderType != null ? String(incomingEvent.senderType) : undefined,
|
|
615
|
+
}) as ToolCallResult;
|
|
530
616
|
if (!r.ok) return { type: 'tool_error', tool, error: r.error || 'unknown', senderId: 'system' };
|
|
531
617
|
// Tools may emit terminal markers (e.g. `message_sent`, `waited`) via
|
|
532
618
|
// `events` — propagate so the loop can detect terminal turns.
|
|
@@ -587,38 +673,6 @@ function pickReceiverForEcho(context: ContextSnapshot, incomingEvent: RawEvent |
|
|
|
587
673
|
return pickPrimaryUserIdFromContext(context);
|
|
588
674
|
}
|
|
589
675
|
|
|
590
|
-
function extractIncomingMessageText(
|
|
591
|
-
incomingEvent: RawEvent | null,
|
|
592
|
-
ctx: ContextSnapshot,
|
|
593
|
-
): string {
|
|
594
|
-
const parts: string[] = [];
|
|
595
|
-
const walk = (ev: RawEvent | null | undefined): void => {
|
|
596
|
-
if (!ev) return;
|
|
597
|
-
if (ev.text) parts.push(String(ev.text));
|
|
598
|
-
if (typeof ev.message === 'string') parts.push(ev.message);
|
|
599
|
-
const result = ev.result as Record<string, unknown> | undefined;
|
|
600
|
-
if (result) {
|
|
601
|
-
if (result.description) parts.push(String(result.description));
|
|
602
|
-
const ag = result.agreement as Record<string, unknown> | undefined;
|
|
603
|
-
if (ag) {
|
|
604
|
-
const terms = ag.terms as Record<string, unknown> | undefined;
|
|
605
|
-
if (terms?.description) parts.push(String(terms.description));
|
|
606
|
-
if (ag.agreementId) parts.push(String(ag.agreementId));
|
|
607
|
-
}
|
|
608
|
-
if (result.agreementId) parts.push(String(result.agreementId));
|
|
609
|
-
}
|
|
610
|
-
if (Array.isArray(ev.events)) (ev.events as RawEvent[]).forEach(walk);
|
|
611
|
-
};
|
|
612
|
-
walk(incomingEvent);
|
|
613
|
-
const hist = ctx.history as Array<Record<string, unknown>> | undefined;
|
|
614
|
-
if (hist?.length) {
|
|
615
|
-
const last = hist[hist.length - 1];
|
|
616
|
-
const text = last?.text ?? last?.content;
|
|
617
|
-
if (text) parts.push(String(text));
|
|
618
|
-
}
|
|
619
|
-
return parts.join('\n').trim();
|
|
620
|
-
}
|
|
621
|
-
|
|
622
676
|
function pickPrimaryUserIdFromContext(context: ContextSnapshot): string | null {
|
|
623
677
|
const users = context?.users || [];
|
|
624
678
|
for (const u of users as Record<string, unknown>[]) {
|
|
@@ -156,6 +156,22 @@ export function validateWorkflow(workflow: Workflow): ValidationResult {
|
|
|
156
156
|
`thinking state "${name}" has no fallthrough for { kind: 'wait' } — agent may stall when LLM picks wait. Spread thinkingDefaults().transitions or add explicitly.`,
|
|
157
157
|
);
|
|
158
158
|
}
|
|
159
|
+
const hasErrorFallthrough = state.transitions.some(t => {
|
|
160
|
+
if (!t.when) return true;
|
|
161
|
+
try {
|
|
162
|
+
return t.when(
|
|
163
|
+
{ kind: 'error', source: 'unknown', cause: 'probe', retryable: false },
|
|
164
|
+
PROBE_CTX,
|
|
165
|
+
);
|
|
166
|
+
} catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
if (!hasErrorFallthrough) {
|
|
171
|
+
warnings.push(
|
|
172
|
+
`thinking state "${name}" has no fallthrough for { kind: 'error' } — empty/failed turns may silently recurse until the depth guard (ZIG-303). Add an error transition or spread thinkingDefaults().transitions.`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
159
175
|
}
|
|
160
176
|
}
|
|
161
177
|
}
|