@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,364 @@
1
+ import type { Ctx, Outcome, Workflow, Identity, EffectHandler } from '../types.js';
2
+ import { runtimeLog } from '../shared/runtimeLog.js';
3
+ import { applyOutcomeToCtx, outcomeFromEvent, type RawEvent } from './buildOutcome.js';
4
+ import type { runTurn as RunTurnFn } from './runTurn.js';
5
+ import { PromptBuilder } from './PromptBuilder.js';
6
+ import type { MemoryStore } from '../memory/MemoryStore.js';
7
+ import type { ToolManager } from '../tools/ToolManager.js';
8
+
9
+ export interface MachineSnapshot {
10
+ value: string;
11
+ ctx: Ctx;
12
+ status: 'active' | 'stopped';
13
+ }
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
+
21
+ interface MachineInput {
22
+ identity: Identity;
23
+ /** Server-provided ctx for an existing session. If omitted, a fresh ctx is built. */
24
+ ctx?: Ctx;
25
+ /** Server-provided state name for an existing session. Defaults to workflow.initial. */
26
+ state?: string;
27
+ }
28
+
29
+ interface MachineOptions {
30
+ workflow: Workflow;
31
+ executionCore: typeof RunTurnFn;
32
+ /** Side channel for all I/O. Provided by the Server. */
33
+ eff: EffectHandler;
34
+ /** Pure tool metadata (schemas, flags). Owned by the Agent. */
35
+ toolManager: ToolManager;
36
+ promptBuilder: PromptBuilder;
37
+ /** Agent-private long-term memory. Operator-provided. */
38
+ memory?: MemoryStore;
39
+ input: MachineInput;
40
+ /** Optional tap for FSM transition events — called synchronously on each transition. */
41
+ transitionTap?: (event: TransitionEvent) => void;
42
+ }
43
+
44
+ interface IncomingFrame {
45
+ event?: RawEvent;
46
+ identity?: Identity;
47
+ }
48
+
49
+ type Subscriber = (snapshot: MachineSnapshot) => void;
50
+
51
+ /**
52
+ * Per-lane interpreter. Holds (current state name, Ctx). Re-entry is the
53
+ * single primitive: every external event and every turn produces exactly
54
+ * one Outcome that re-enters the current state.
55
+ *
56
+ * Parked states stop on entry. Thinking states run an LLM turn → produce an
57
+ * outcome → re-enter. Transitions are evaluated against the new outcome
58
+ * (first matching `when` wins; no `when` is fallthrough).
59
+ *
60
+ * `_running` guards against concurrent turns on the same machine — outer
61
+ * runtime is expected to serialize via EventQueue, but we belt-and-suspenders.
62
+ */
63
+ export class AgentMachine {
64
+ workflow: Workflow;
65
+ executionCore: typeof RunTurnFn;
66
+ eff: EffectHandler;
67
+ toolManager: ToolManager;
68
+ promptBuilder: PromptBuilder;
69
+ memory?: MemoryStore;
70
+ state: string;
71
+ status: 'active' | 'stopped' = 'active';
72
+ ctx: Ctx;
73
+ _running = false;
74
+ _subscribers = new Set<Subscriber>();
75
+ _prevSnapshot: MachineSnapshot | null = null;
76
+ _pendingFrames: IncomingFrame[] = [];
77
+ _draining = false;
78
+ _llmCalls = 0;
79
+ _tokens = { total: 0, input: 0, output: 0 };
80
+ _lastUserSenderId: string | null = null;
81
+ _transitionTap: ((event: TransitionEvent) => void) | undefined;
82
+
83
+ get llmCalls(): number { return this._llmCalls; }
84
+ get tokens(): { total: number; input: number; output: number } { return this._tokens; }
85
+
86
+ constructor({ workflow, executionCore, eff, toolManager, promptBuilder, memory, input, transitionTap }: MachineOptions) {
87
+ this.workflow = workflow;
88
+ this.executionCore = executionCore;
89
+ this.eff = eff;
90
+ this.toolManager = toolManager;
91
+ this.promptBuilder = promptBuilder;
92
+ this.memory = memory;
93
+ this._transitionTap = transitionTap;
94
+ this.state = input.state || workflow.initial;
95
+ this.ctx = input.ctx
96
+ ? { ...input.ctx, identity: input.identity }
97
+ : {
98
+ identity: input.identity,
99
+ activeAgreementId: null,
100
+ activeTaskId: null,
101
+ pendingProposalAgreementId: null,
102
+ delegatedAgreementIds: [],
103
+ delegatedTaskIds: [],
104
+ toolResults: [],
105
+ lastOutcome: { kind: 'enter' },
106
+ };
107
+ }
108
+
109
+ getSnapshot(): MachineSnapshot {
110
+ return { value: this.state, ctx: this.ctx, status: this.status };
111
+ }
112
+
113
+ /** Parked = currently in a parked state and not executing a turn. */
114
+ get isParked(): boolean {
115
+ if (this._running) return false;
116
+ const sd = this.workflow.states[this.state];
117
+ return sd?.kind === 'parked';
118
+ }
119
+
120
+ subscribe(fn: Subscriber): { unsubscribe: () => void } {
121
+ this._subscribers.add(fn);
122
+ return { unsubscribe: () => this._subscribers.delete(fn) };
123
+ }
124
+
125
+ /**
126
+ * Process an incoming wire event: convert to outcome → apply to ctx →
127
+ * evaluate current state's transitions → re-enter or stop.
128
+ *
129
+ * Serialized via a single-slot queue: if a turn is already in flight, the
130
+ * frame is held until the running turn settles, then drained. Without this,
131
+ * an inbound event mid-turn re-entered _step with a stale state and a hot
132
+ * `_running=true`, which spawned overlapping turns and re-created the
133
+ * clarifyingDuringApproval feedback loop even after fixing the workflow
134
+ * fall-through (ZIG-181).
135
+ */
136
+ async send(frame: IncomingFrame): Promise<void> {
137
+ if (this.status !== 'active') return;
138
+ this._pendingFrames.push(frame);
139
+ if (this._draining) return;
140
+ this._draining = true;
141
+ try {
142
+ while (this._pendingFrames.length) {
143
+ const next = this._pendingFrames.shift()!;
144
+ await this._handleFrame(next);
145
+ }
146
+ } finally {
147
+ this._draining = false;
148
+ }
149
+ }
150
+
151
+ async _handleFrame(frame: IncomingFrame): Promise<void> {
152
+ if (this.status !== 'active') return;
153
+ if (frame.identity) this.ctx.identity = frame.identity;
154
+
155
+ const ev = frame.event;
156
+ this._lastIncomingEvent = ev ?? null;
157
+ if (ev?.senderId && String(ev?.senderType ?? '').toUpperCase() === 'USER') {
158
+ this._lastUserSenderId = String(ev.senderId);
159
+ }
160
+ runtimeLog.debug(
161
+ 'FSM',
162
+ `_handleFrame ev.type=${ev?.type} ev.senderId=${ev?.senderId} ev.receiverId=${ev?.receiverId} myId=${this.ctx.identity?.agentId}`,
163
+ );
164
+ const outcome = outcomeFromEvent(ev, this.ctx.identity.agentId);
165
+
166
+ // `outcomeFromEvent` never returns null — it falls back to `{ kind: 'enter' }`
167
+ // when all events in a batch are no-ops (resource_changed, self-echo, etc.).
168
+ // If a real raw event produced `enter`, that means the event carried no
169
+ // meaningful signal for the FSM, so skip it entirely. Without this guard
170
+ // `idle`'s unconditional `{ to: 'understanding' }` transition fires on every
171
+ // resource_changed wake-up, kicking off an LLM turn that sends a duplicate
172
+ // message, which triggers another resource_changed, and so on.
173
+ if (ev && outcome.kind === 'enter') return;
174
+
175
+ if (
176
+ outcome.kind === 'subtask-finished' &&
177
+ ev?.type === 'task_result' &&
178
+ ev?.result?.taskId &&
179
+ this.ctx.delegatedTaskIds.includes(ev.result.taskId)
180
+ ) {
181
+ // Already correctly classified — no override needed.
182
+ }
183
+
184
+ applyOutcomeToCtx(this.ctx, outcome);
185
+ await this._step(outcome);
186
+ }
187
+
188
+ async _step(outcome: Outcome, depth = 0): Promise<void> {
189
+ const fromState = this.state;
190
+ const target = this._evaluateTransitions(outcome);
191
+ const o = outcome as Record<string, unknown>;
192
+ runtimeLog.debug(
193
+ 'FSM',
194
+ `_step from=${fromState} outcome.kind=${o?.kind} senderId=${o?.senderId ?? '<none>'} myId=${this.ctx.identity?.agentId ?? '<none>'} → target=${target ?? '(stay)'} depth=${depth}`,
195
+ );
196
+ if (target && target !== this.state) {
197
+ this._transitionTap?.({ kind: 'transition', fromState, toState: target, outcomeKind: (outcome as Record<string, unknown>).kind as string, depth });
198
+ this.state = target;
199
+ this._notify();
200
+ }
201
+
202
+ const sd = this.workflow.states[this.state];
203
+ if (!sd) return;
204
+ if (sd.kind === 'parked') {
205
+ runtimeLog.debug('FSM', `_step parked state=${this.state} — exit`);
206
+ // Notify so runtime detects park and resolves the wait promise.
207
+ this._notify();
208
+ return;
209
+ }
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
+
231
+ // Recursion / runaway-turn guard. Without this, a thinking state whose
232
+ // transitions don't match the turn outcome (e.g. `delegating` not
233
+ // catching `tool-result` from an agent_network search) re-enters the
234
+ // same state forever — the LLM keeps re-rolling search/dummy actions
235
+ // until something accidentally matches, burning tokens and emitting
236
+ // duplicate side effects (ZIG-181: ziggs ran `discoverSpecialist` six
237
+ // times before finally invoking `delegate`). 12 is well above any
238
+ // legitimate same-state turn chain we exercise in tests; if we trip
239
+ // it, treat the state as wedged and park.
240
+ if (depth >= 12) {
241
+ runtimeLog.warn(
242
+ 'AgentMachine',
243
+ `state=${this.state} runaway turn loop (depth=${depth}); parking`,
244
+ );
245
+ this._transitionTap?.({ kind: 'depth-guard', state: this.state, depth });
246
+ await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
247
+ this._notify();
248
+ return;
249
+ }
250
+ runtimeLog.debug('FSM', `_step thinking state=${this.state} — run turn`);
251
+
252
+ // Thinking: run a turn, produce an outcome, re-step.
253
+ this._running = true;
254
+ try {
255
+ const { outcome: turnOutcome, toolResults, llmCalls, tokens } = await this.executionCore({
256
+ stateId: this.state,
257
+ statePrompt: sd.prompt,
258
+ actions: sd.actions,
259
+ incomingEvent: this._lastIncomingEvent,
260
+ ctx: this.ctx,
261
+ eff: this.eff,
262
+ toolManager: this.toolManager,
263
+ promptBuilder: this.promptBuilder,
264
+ memory: this.memory,
265
+ definition: this.workflow,
266
+ });
267
+
268
+ this.ctx.toolResults = toolResults;
269
+ this._llmCalls += llmCalls;
270
+ this._tokens.total += tokens.total;
271
+ this._tokens.input += tokens.input;
272
+ this._tokens.output += tokens.output;
273
+ applyOutcomeToCtx(this.ctx, turnOutcome);
274
+ this._running = false;
275
+ this._notify();
276
+
277
+ await this._step(turnOutcome, depth + 1);
278
+ } catch (error: unknown) {
279
+ runtimeLog.error('AgentMachine', `state=${this.state} error: ${(error as Error)?.message || error}`);
280
+ const errOutcome: Outcome = {
281
+ kind: 'error',
282
+ source: 'unknown',
283
+ cause: (error as Error)?.message || String(error),
284
+ retryable: false,
285
+ };
286
+ applyOutcomeToCtx(this.ctx, errOutcome);
287
+ this._running = false;
288
+ this._notify();
289
+ // Try to fall through using current state's transitions; if none match
290
+ // the error outcome the machine just parks (next event will re-enter).
291
+ const fallback = this._evaluateTransitions(errOutcome);
292
+ if (fallback && fallback !== this.state) {
293
+ this.state = fallback;
294
+ this._notify();
295
+ } else {
296
+ await this._sendFallbackIfUserWaiting('Something went wrong. Please try again.');
297
+ }
298
+ }
299
+ }
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
+
328
+ _evaluateTransitions(outcome: Outcome): string | null {
329
+ const sd = this.workflow.states[this.state];
330
+ if (!sd?.transitions?.length) {
331
+ runtimeLog.debug('FSM', `evaluate state=${this.state}: NO TRANSITIONS DEFINED`);
332
+ return null;
333
+ }
334
+ for (let i = 0; i < sd.transitions.length; i++) {
335
+ const rule = sd.transitions[i]!;
336
+ let matched: boolean;
337
+ let err: unknown = null;
338
+ if (!rule.when) {
339
+ matched = true;
340
+ } else {
341
+ try { matched = !!rule.when(outcome, this.ctx); }
342
+ catch (e) { matched = false; err = e; }
343
+ }
344
+ runtimeLog.debug(
345
+ 'FSM',
346
+ `evaluate state=${this.state} rule[${i}] to=${rule.to} when=${rule.when ? 'fn' : 'always'} → ${matched}${err ? ` ERR=${(err as Error).message}` : ''}`,
347
+ );
348
+ if (matched) return rule.to;
349
+ }
350
+ runtimeLog.debug('FSM', `evaluate state=${this.state}: NONE matched`);
351
+ return null;
352
+ }
353
+
354
+ _notify(): void {
355
+ const snapshot = this.getSnapshot();
356
+ for (const fn of this._subscribers) {
357
+ try { fn(snapshot); } catch {}
358
+ }
359
+ }
360
+
361
+ // Held so PromptBuilder/runTurn can render the original wire event in <event>.
362
+ // Set by the runtime layer when it calls send(); we don't classify it twice.
363
+ _lastIncomingEvent: RawEvent | null = null;
364
+ }