@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.
- package/README.md +3 -1
- package/package.json +9 -4
- package/src/AgentHost.ts +495 -0
- package/src/adapters/OpenAIAdapter.ts +146 -0
- package/src/agent/Agent.ts +101 -0
- package/src/cognition/validateContext.ts +95 -0
- package/src/context/applyEffects.ts +80 -0
- package/src/context/batch.ts +17 -0
- package/src/context/classifyEnvelope.ts +38 -0
- package/src/context/routingLabels.ts +46 -0
- package/src/defineAgent.ts +62 -0
- package/src/formatters/AgreementFormatter.ts +111 -0
- package/src/formatters/HistoryFormatter.ts +166 -0
- package/src/formatters/index.ts +2 -0
- package/src/index.ts +105 -0
- package/src/ingress/normalizeIncoming.ts +162 -0
- package/src/memory/MemoryStore.ts +104 -0
- 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 +364 -0
- package/src/runtime/PromptBuilder.ts +463 -0
- package/src/runtime/buildOutcome.ts +518 -0
- package/src/runtime/defaults.ts +75 -0
- package/src/runtime/runTurn.ts +691 -0
- package/src/runtime/validateWorkflow.ts +181 -0
- package/src/server/ConnectionPool.ts +155 -0
- package/src/server/EventQueue.ts +133 -0
- package/src/server/InboxCatchUp.ts +251 -0
- package/src/server/OutboxBuffer.ts +90 -0
- package/src/server/SeenMessages.ts +27 -0
- package/src/server/ZiggsEffectHandler.ts +409 -0
- package/src/server/agreements/AgreementService.ts +117 -0
- package/src/server/createHealthServer.ts +85 -0
- package/src/server/proactive/ProactiveTrigger.ts +83 -0
- package/src/server/runLauncher.ts +146 -0
- package/src/server/tasks/TaskService.ts +110 -0
- package/src/server/tasks/index.ts +1 -0
- package/src/server/telemetryIngest.ts +91 -0
- package/src/server/tools/index.ts +46 -0
- package/src/server/tools/tier1/protocolRunner.ts +133 -0
- package/src/server/tools/tier1/protocolTools.ts +99 -0
- 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/tools/tier2/paymentTools.ts +193 -0
- package/src/server/ziggsconnect/ZiggsConnectClient.ts +126 -0
- package/src/server/ziggscontext/ZiggsContextClient.ts +137 -0
- package/src/server/ziggspay/ZiggsPayClient.ts +193 -0
- package/src/shared/ids.ts +3 -0
- package/src/shared/runtimeLog.ts +72 -0
- package/src/shared/types.ts +29 -0
- package/src/tasks/protocolRegistry.ts +25 -0
- package/src/tools/ToolManager.ts +95 -0
- package/src/tools/{ToolProvider.js → ToolProvider.ts} +5 -15
- package/src/tools/defineTool.ts +90 -0
- package/src/tools/index.ts +5 -0
- package/src/types.ts +407 -0
- package/src/utils/jsonExtractor.ts +100 -0
- package/src/ConnectionPool.js +0 -133
- package/src/adapters/OpenAIAdapter.js +0 -73
- package/src/agent/Agent.js +0 -121
- package/src/agent/EventQueue.js +0 -68
- package/src/agent/OutboxBuffer.js +0 -62
- package/src/cognition/PromptBuilder.js +0 -312
- package/src/cognition/resolveActionTool.js +0 -12
- package/src/cognition/runTurn.js +0 -578
- package/src/context/applyEffects.js +0 -133
- package/src/context/batch.js +0 -25
- package/src/context/classifyEnvelope.js +0 -82
- package/src/context/routingLabels.js +0 -54
- package/src/createHealthServer.js +0 -28
- package/src/formatters/HistoryFormatter.js +0 -257
- package/src/formatters/TaskFormatter.js +0 -180
- package/src/formatters/index.js +0 -9
- package/src/index.js +0 -76
- package/src/ingress/normalizeIncoming.js +0 -70
- package/src/runLauncher.js +0 -159
- package/src/shared/ids.js +0 -7
- package/src/shared/types.js +0 -86
- package/src/tasks/TaskService.js +0 -247
- package/src/tasks/index.js +0 -9
- package/src/tasks/taskCore.js +0 -229
- package/src/tasks/taskProtocolRegistry.js +0 -22
- package/src/tasks/taskProtocolRunner.js +0 -107
- package/src/tasks/taskProtocolTools.js +0 -87
- package/src/tools/ToolManager.js +0 -79
- package/src/tools/defineTool.js +0 -82
- package/src/tools/index.js +0 -11
- package/src/utils/jsonExtractor.js +0 -139
- package/src/workflow/AgentMachine.js +0 -250
- package/src/workflow/WorkflowRuntime.js +0 -63
- package/src/workflow/dsl.js +0 -287
- package/src/workflow/motifs.js +0 -435
- package/src/ziggs/runtime.js +0 -192
- /package/src/adapters/{index.js → index.ts} +0 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Action,
|
|
3
|
+
AgreementRef,
|
|
4
|
+
Ctx,
|
|
5
|
+
Outcome,
|
|
6
|
+
OutcomeKind,
|
|
7
|
+
ProposalRef,
|
|
8
|
+
TaskRef,
|
|
9
|
+
ToolResultEntry,
|
|
10
|
+
} from '../types.js';
|
|
11
|
+
import { isProtocolToolName, mapProtocolToolToOperation } from '../tasks/protocolRegistry.js';
|
|
12
|
+
import { getBatchEvents, isTaskResultRelevantToAgent } from '../context/batch.js';
|
|
13
|
+
|
|
14
|
+
// ── Event shape (untyped wire payload normalized into a small set of types) ──
|
|
15
|
+
|
|
16
|
+
/** Wire payload from the backend for task/agreement state. */
|
|
17
|
+
export interface WireResult {
|
|
18
|
+
taskId?: string;
|
|
19
|
+
state?: string;
|
|
20
|
+
status?: string;
|
|
21
|
+
agreement?: AgreementRef;
|
|
22
|
+
agreementId?: string;
|
|
23
|
+
proposal?: { status?: 'pending' | 'approved' | 'rejected' | 'cancelled' };
|
|
24
|
+
task?: TaskRef;
|
|
25
|
+
success?: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
providerIsYou?: boolean;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface RawEvent {
|
|
32
|
+
type?: string;
|
|
33
|
+
result?: WireResult;
|
|
34
|
+
receiverId?: string;
|
|
35
|
+
senderId?: string;
|
|
36
|
+
text?: string;
|
|
37
|
+
events?: RawEvent[];
|
|
38
|
+
[key: string]: unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface EmittedEvent {
|
|
42
|
+
type: string;
|
|
43
|
+
tool?: string;
|
|
44
|
+
result?: WireResult;
|
|
45
|
+
error?: string;
|
|
46
|
+
receiverId?: string;
|
|
47
|
+
chatId?: string;
|
|
48
|
+
message?: string;
|
|
49
|
+
text?: string;
|
|
50
|
+
operation?: string;
|
|
51
|
+
senderId?: string;
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── outcomeFromEvent: incoming wire event → Outcome ──────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Converts a raw incoming event into the single Outcome that re-enters the
|
|
59
|
+
* machine.
|
|
60
|
+
*
|
|
61
|
+
* For batched events, returns the *most significant* outcome (task assignments
|
|
62
|
+
* win over messages). EventQueue already flattened multiple distinct events
|
|
63
|
+
* via re-entry, so a batch here usually means events that arrived together
|
|
64
|
+
* within a single backend frame.
|
|
65
|
+
*/
|
|
66
|
+
export function outcomeFromEvent(
|
|
67
|
+
rawEvent: RawEvent | null | undefined,
|
|
68
|
+
ownAgentId: string | null,
|
|
69
|
+
): Outcome {
|
|
70
|
+
if (!rawEvent) return { kind: 'enter' };
|
|
71
|
+
|
|
72
|
+
const events: RawEvent[] = getBatchEvents(rawEvent) as RawEvent[];
|
|
73
|
+
let best: Outcome | null = null;
|
|
74
|
+
|
|
75
|
+
for (const ev of events) {
|
|
76
|
+
if (!ev) continue;
|
|
77
|
+
const candidate = singleEventToOutcome(ev, ownAgentId);
|
|
78
|
+
if (!candidate) continue;
|
|
79
|
+
if (!best || outcomeRank(candidate) > outcomeRank(best)) best = candidate;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return best ?? { kind: 'enter' };
|
|
83
|
+
}
|
|
84
|
+
|
|
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
|
+
|
|
105
|
+
if (ev.type === 'task_result') {
|
|
106
|
+
const result = ev.result || {};
|
|
107
|
+
if (
|
|
108
|
+
ownAgentId &&
|
|
109
|
+
!isTaskResultRelevantToAgent(result, ownAgentId) &&
|
|
110
|
+
ev.receiverId !== ownAgentId
|
|
111
|
+
) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Agreement lifecycle notification routed as task_result by normalizeIncomingEvent.
|
|
116
|
+
// _operation is set to the backend operation name (e.g. 'proposal_approved').
|
|
117
|
+
const inboundOp = result._operation as string | undefined;
|
|
118
|
+
if (inboundOp) {
|
|
119
|
+
const proposalAction = inboundOp === 'proposal_approved' ? 'approve'
|
|
120
|
+
: inboundOp === 'proposal_rejected' ? 'reject'
|
|
121
|
+
: null;
|
|
122
|
+
if (proposalAction) {
|
|
123
|
+
const agId = result.agreementId;
|
|
124
|
+
const proposal: ProposalRef = agId
|
|
125
|
+
? { agreementId: agId, agreement: result as unknown as AgreementRef }
|
|
126
|
+
: { agreementId: '' };
|
|
127
|
+
return { kind: 'proposal-resolved', action: proposalAction as 'approve' | 'reject', proposal };
|
|
128
|
+
}
|
|
129
|
+
// Other lifecycle ops (countered, expired) treated as enter — no FSM action needed here.
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Tasks are born active in the new model — no `proposal` task state.
|
|
134
|
+
// The wire shape `task_result` always carries an active or terminal task.
|
|
135
|
+
const task: TaskRef = result as unknown as TaskRef;
|
|
136
|
+
const ag: AgreementRef | undefined = result.agreement;
|
|
137
|
+
const parties = ag?.parties || {};
|
|
138
|
+
const state = result.state;
|
|
139
|
+
|
|
140
|
+
// Direct assignment to me (provider) — task spawned, citing an active agreement.
|
|
141
|
+
const isAssignedToMe =
|
|
142
|
+
(result.providerIsYou || parties.provider === ownAgentId) &&
|
|
143
|
+
(state === 'active' || state === 'in-progress');
|
|
144
|
+
if (isAssignedToMe) {
|
|
145
|
+
return { kind: 'task-assigned', task };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (state === 'active' || state === 'in-progress') {
|
|
149
|
+
return { kind: 'task-assigned', task };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (state === 'completed' || state === 'failed') {
|
|
153
|
+
return {
|
|
154
|
+
kind: 'subtask-finished',
|
|
155
|
+
status: state === 'completed' ? 'completed' : 'failed',
|
|
156
|
+
task,
|
|
157
|
+
result,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (state === 'cancelled') {
|
|
162
|
+
return {
|
|
163
|
+
kind: 'subtask-finished',
|
|
164
|
+
status: 'failed',
|
|
165
|
+
task,
|
|
166
|
+
result,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Fallback: addressed to us with no clear state.
|
|
171
|
+
if (ownAgentId && ev.receiverId === ownAgentId) {
|
|
172
|
+
return { kind: 'task-assigned', task };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { kind: 'subtask-finished', status: 'completed', task, result };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (ev.type === 'resource_changed') {
|
|
179
|
+
const resource = ((ev as { resource?: Record<string, unknown> }).resource ?? ev) as {
|
|
180
|
+
kind?: string;
|
|
181
|
+
reason?: string;
|
|
182
|
+
resourceId?: string;
|
|
183
|
+
agreementId?: string;
|
|
184
|
+
taskId?: string;
|
|
185
|
+
};
|
|
186
|
+
const reason = resource.reason;
|
|
187
|
+
if (
|
|
188
|
+
resource.kind === 'agreement' &&
|
|
189
|
+
(reason === 'proposal_approved' || reason === 'proposal_rejected')
|
|
190
|
+
) {
|
|
191
|
+
const agreementId = resource.agreementId ?? resource.resourceId ?? '';
|
|
192
|
+
const proposal: ProposalRef = { agreementId };
|
|
193
|
+
return {
|
|
194
|
+
kind: 'proposal-resolved',
|
|
195
|
+
action: reason === 'proposal_approved' ? 'approve' : 'reject',
|
|
196
|
+
proposal,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (resource.kind === 'task-state' && reason === 'subtask_completed') {
|
|
200
|
+
const taskId = resource.taskId ?? resource.resourceId ?? '';
|
|
201
|
+
const task = { taskId, state: 'completed' } as TaskRef;
|
|
202
|
+
return { kind: 'subtask-finished', status: 'completed', task, result: task };
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Only real inbound chat messages should drive the FSM. Backend bookkeeping
|
|
208
|
+
// (presence/heartbeat, raw `batch` outer frames, etc.) are NOT chat traffic.
|
|
209
|
+
if (ev.type !== 'message') return null;
|
|
210
|
+
|
|
211
|
+
// Suppress our own messages echoed back through the chat fanout.
|
|
212
|
+
if (ownAgentId && ev.senderId && ev.senderId === ownAgentId) return null;
|
|
213
|
+
|
|
214
|
+
const text = typeof ev.text === 'string' ? ev.text : undefined;
|
|
215
|
+
const senderType = typeof ev.senderType === 'string' ? ev.senderType : undefined;
|
|
216
|
+
return { kind: 'message-received', text, senderId: ev.senderId, senderType };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Higher rank wins when collapsing batched events into one outcome.
|
|
220
|
+
function outcomeRank(o: Outcome): number {
|
|
221
|
+
switch (o.kind) {
|
|
222
|
+
case 'task-assigned': return 100;
|
|
223
|
+
case 'proposal-resolved': return 90;
|
|
224
|
+
case 'subtask-finished': return 80;
|
|
225
|
+
case 'message-received': return 10;
|
|
226
|
+
default: return 50;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ── outcomeFromActionResult: turn output → Outcome ───────────────────────────
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Builds the single Outcome that ends a thinking-state turn from the
|
|
234
|
+
* action that ran and the events it emitted.
|
|
235
|
+
*
|
|
236
|
+
* Action's `produces` declares the outcome kind — runtime fills in payload.
|
|
237
|
+
* - static string: kind is fixed; runtime constructs payload from events.
|
|
238
|
+
* - function: receives the tool result and args, returns the kind.
|
|
239
|
+
*
|
|
240
|
+
* If `produces` is missing or doesn't match the events, falls back to a
|
|
241
|
+
* best-effort inference.
|
|
242
|
+
*/
|
|
243
|
+
export function outcomeFromActionResult(
|
|
244
|
+
actionName: string | null,
|
|
245
|
+
action: Action | null,
|
|
246
|
+
args: unknown,
|
|
247
|
+
emittedEvents: EmittedEvent[],
|
|
248
|
+
toolResultsOut: ToolResultEntry[],
|
|
249
|
+
): Outcome {
|
|
250
|
+
if (!emittedEvents.length) {
|
|
251
|
+
return { kind: 'error', source: 'unknown', cause: `action "${actionName}" emitted nothing`, retryable: false };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Collect tool results in caller's accumulator (used in prompts and ctx).
|
|
255
|
+
for (const e of emittedEvents) {
|
|
256
|
+
if (e?.type === 'tool_result') toolResultsOut.push({ tool: e.tool || '', result: e.result });
|
|
257
|
+
if (e?.type === 'tool_error') toolResultsOut.push({ tool: e.tool || '', error: e.error });
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Pick the "primary" event the outcome describes.
|
|
261
|
+
//
|
|
262
|
+
// Protocol-level state changes (agreement creation, task assignment/closure,
|
|
263
|
+
// proposal response) outrank EVERYTHING else, including message_sent and
|
|
264
|
+
// waited. Why: when the LLM batches `delegate` + `wait` in one round, the
|
|
265
|
+
// sub-agreement DOES get created server-side, but if `waited` wins the FSM
|
|
266
|
+
// sees `wait` and bounces back to `idle` instead of transitioning to
|
|
267
|
+
// `awaitingSubAgreementApproval` — and the chain stalls. The meaningful
|
|
268
|
+
// turn outcome is the work that landed, not the LLM's terminal decision.
|
|
269
|
+
//
|
|
270
|
+
// Among non-protocol events: message_sent / waited still outrank pure
|
|
271
|
+
// query tool_results (e.g. agent_network search) and tool_errors, because
|
|
272
|
+
// those are the LLM's explicit final intent in turns that did no protocol
|
|
273
|
+
// work.
|
|
274
|
+
const primary =
|
|
275
|
+
emittedEvents.find(e => e.type === 'task_result' && isStateChangingProtocolEvent(e)) ??
|
|
276
|
+
emittedEvents.find(e => e.type === 'tool_result' && isStateChangingProtocolEvent(e)) ??
|
|
277
|
+
emittedEvents.find(e => e.type === 'message_sent') ??
|
|
278
|
+
emittedEvents.find(e => e.type === 'message_duplicate_skipped') ??
|
|
279
|
+
emittedEvents.find(e => e.type === 'waited') ??
|
|
280
|
+
emittedEvents.find(e => e.type === 'tool_result') ??
|
|
281
|
+
emittedEvents.find(e => e.type === 'tool_error') ??
|
|
282
|
+
emittedEvents.find(e => e.type === 'task_result') ??
|
|
283
|
+
emittedEvents.find(e => e.type === 'task_error') ??
|
|
284
|
+
emittedEvents[0];
|
|
285
|
+
|
|
286
|
+
// Resolve the kind via produces (preferred) or by inferring from primary.
|
|
287
|
+
let kind: OutcomeKind | null = null;
|
|
288
|
+
if (action?.produces) {
|
|
289
|
+
const p = action.produces;
|
|
290
|
+
const result = primary?.type === 'tool_result' ? primary.result : undefined;
|
|
291
|
+
kind = typeof p === 'function' ? p(result, args) : p;
|
|
292
|
+
}
|
|
293
|
+
if (!kind) kind = inferKindFromEvent(primary);
|
|
294
|
+
|
|
295
|
+
return buildOutcomeOfKind(kind, primary, action);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Whether a tool_result / task_result represents a protocol state change
|
|
300
|
+
* (agreement creation, task assignment / closure, proposal response). Used
|
|
301
|
+
* to outrank pure query tool_results (e.g. `agent_network search`) when the
|
|
302
|
+
* same turn batches multiple tools.
|
|
303
|
+
*/
|
|
304
|
+
function isStateChangingProtocolEvent(ev: EmittedEvent): boolean {
|
|
305
|
+
const op = ev.type === 'task_result'
|
|
306
|
+
? ev.operation
|
|
307
|
+
: (ev.tool && isProtocolToolName(ev.tool) ? mapProtocolToolToOperation(ev.tool) : null);
|
|
308
|
+
if (!op) return false;
|
|
309
|
+
const r: WireResult = ev.result ?? ({} as WireResult);
|
|
310
|
+
if (r.success === false || r.error) return false;
|
|
311
|
+
const ag = r.agreement ?? (r.agreementId ? r : null);
|
|
312
|
+
const task = r.task || (r.taskId ? r : null);
|
|
313
|
+
const proposalStatus = ag?.proposal?.status;
|
|
314
|
+
const state = task?.state || r.state;
|
|
315
|
+
if (op === 'agreement-propose' || op === 'agreement-subcontract') {
|
|
316
|
+
return proposalStatus === 'pending' || proposalStatus === 'approved' || !!ag?.agreementId;
|
|
317
|
+
}
|
|
318
|
+
if (op === 'agreement-respond') return true;
|
|
319
|
+
if (op === 'task-update') {
|
|
320
|
+
return state === 'completed' || state === 'failed' || state === 'cancelled';
|
|
321
|
+
}
|
|
322
|
+
if (op === 'task-spawn') return !!task?.taskId;
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function inferKindFromEvent(primary: EmittedEvent | undefined): OutcomeKind {
|
|
327
|
+
if (!primary) return 'error';
|
|
328
|
+
switch (primary.type) {
|
|
329
|
+
case 'message_sent': return 'message-sent';
|
|
330
|
+
case 'message_duplicate_skipped': return 'message-sent';
|
|
331
|
+
case 'waited': return 'wait';
|
|
332
|
+
case 'tool_error': return 'error';
|
|
333
|
+
case 'task_error': return 'error';
|
|
334
|
+
case 'tool_result': return inferKindFromToolResult(primary);
|
|
335
|
+
case 'task_result': return inferKindFromTaskResult(primary);
|
|
336
|
+
default: return 'tool-result';
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function inferKindFromToolResult(ev: EmittedEvent): OutcomeKind {
|
|
341
|
+
if (!isProtocolToolName(ev.tool ?? '')) return 'tool-result';
|
|
342
|
+
const op = mapProtocolToolToOperation(ev.tool ?? '');
|
|
343
|
+
return inferKindFromProtocolOp(op ?? undefined, ev.result);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function inferKindFromTaskResult(ev: EmittedEvent): OutcomeKind {
|
|
347
|
+
return inferKindFromProtocolOp(ev.operation, ev.result);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function inferKindFromProtocolOp(op: string | undefined, result: unknown): OutcomeKind {
|
|
351
|
+
const r: WireResult = (result as WireResult) || ({} as WireResult);
|
|
352
|
+
const ag: AgreementRef | undefined = r.agreement ?? (r.agreementId ? r as unknown as AgreementRef : undefined);
|
|
353
|
+
const task: TaskRef | undefined = r.task || (r.taskId ? r as unknown as TaskRef : undefined);
|
|
354
|
+
|
|
355
|
+
// Agreement creation verbs no longer create paired tasks. The caller (creator)
|
|
356
|
+
// has made a proposal awaiting response from the proposedTo party.
|
|
357
|
+
if (
|
|
358
|
+
op === 'agreement-propose' ||
|
|
359
|
+
op === 'agreement-subcontract'
|
|
360
|
+
) {
|
|
361
|
+
return 'proposal-made';
|
|
362
|
+
}
|
|
363
|
+
if (op === 'task-spawn') {
|
|
364
|
+
// A task was just spawned under an active agreement. From the spawner's
|
|
365
|
+
// side this is a delegation; the executor sees `task-assigned` via the
|
|
366
|
+
// task_result event channel.
|
|
367
|
+
return 'task-delegated';
|
|
368
|
+
}
|
|
369
|
+
if (op === 'task-update') {
|
|
370
|
+
const status = task?.state || r.state || r.status;
|
|
371
|
+
if (status === 'completed' || status === 'failed') return 'task-closed';
|
|
372
|
+
return 'tool-result';
|
|
373
|
+
}
|
|
374
|
+
if (op === 'agreement-respond') {
|
|
375
|
+
// The responder's tool result is the agreement after approve/reject.
|
|
376
|
+
return 'proposal-resolved';
|
|
377
|
+
}
|
|
378
|
+
void ag;
|
|
379
|
+
return 'tool-result';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function buildOutcomeOfKind(
|
|
383
|
+
kind: OutcomeKind,
|
|
384
|
+
primary: EmittedEvent | undefined,
|
|
385
|
+
action: Action | null,
|
|
386
|
+
): Outcome {
|
|
387
|
+
const tool = action?.tool || primary?.tool || '';
|
|
388
|
+
const result = primary?.type === 'tool_result' ? primary.result : primary?.result;
|
|
389
|
+
|
|
390
|
+
switch (kind) {
|
|
391
|
+
case 'enter':
|
|
392
|
+
case 'wait':
|
|
393
|
+
case 'timeout':
|
|
394
|
+
return { kind };
|
|
395
|
+
|
|
396
|
+
case 'message-sent':
|
|
397
|
+
return { kind, text: primary?.message, receiverId: primary?.receiverId };
|
|
398
|
+
|
|
399
|
+
case 'message-received':
|
|
400
|
+
return { kind, text: primary?.text };
|
|
401
|
+
|
|
402
|
+
case 'task-assigned': {
|
|
403
|
+
const task: TaskRef = (result?.task || result || {}) as unknown as TaskRef;
|
|
404
|
+
return { kind, task };
|
|
405
|
+
}
|
|
406
|
+
case 'task-delegated': {
|
|
407
|
+
const task: TaskRef = (result?.task || result || {}) as unknown as TaskRef;
|
|
408
|
+
return { kind, task };
|
|
409
|
+
}
|
|
410
|
+
case 'task-closed': {
|
|
411
|
+
const raw = result?.task?.state || result?.state;
|
|
412
|
+
const status: 'completed' | 'failed' = raw === 'failed' ? 'failed' : 'completed';
|
|
413
|
+
return { kind, status, result };
|
|
414
|
+
}
|
|
415
|
+
case 'subtask-finished': {
|
|
416
|
+
const task: TaskRef = (result?.task || result || {}) as unknown as TaskRef;
|
|
417
|
+
const status = (task.state === 'failed' ? 'failed' : 'completed') as 'completed' | 'failed';
|
|
418
|
+
return { kind, status, task, result };
|
|
419
|
+
}
|
|
420
|
+
case 'proposal-made': {
|
|
421
|
+
const ag: AgreementRef | undefined = result?.agreement ?? (result?.agreementId ? result as unknown as AgreementRef : undefined);
|
|
422
|
+
const proposal: ProposalRef = ag
|
|
423
|
+
? { agreementId: ag.agreementId, agreement: ag }
|
|
424
|
+
: { agreementId: result?.agreementId || '' };
|
|
425
|
+
return { kind, proposal };
|
|
426
|
+
}
|
|
427
|
+
case 'proposal-resolved': {
|
|
428
|
+
const ag: AgreementRef | undefined = result?.agreement ?? (result?.agreementId ? result as unknown as AgreementRef : undefined);
|
|
429
|
+
const status = ag?.proposal?.status;
|
|
430
|
+
const action_: 'approve' | 'reject' = status === 'rejected' ? 'reject' : 'approve';
|
|
431
|
+
const proposal: ProposalRef = ag
|
|
432
|
+
? { agreementId: ag.agreementId, agreement: ag }
|
|
433
|
+
: { agreementId: result?.agreementId || '' };
|
|
434
|
+
return { kind, action: action_, proposal };
|
|
435
|
+
}
|
|
436
|
+
case 'tool-result':
|
|
437
|
+
return { kind, tool, result };
|
|
438
|
+
|
|
439
|
+
case 'error': {
|
|
440
|
+
const cause = primary?.error || primary?.result || 'unknown error';
|
|
441
|
+
const source = primary?.type === 'tool_error' || primary?.type === 'tool_result' ? 'tool' : 'unknown';
|
|
442
|
+
return { kind, source, cause, retryable: false };
|
|
443
|
+
}
|
|
444
|
+
case 'extension':
|
|
445
|
+
return { kind, name: tool || 'unknown', payload: result };
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── applyOutcomeToCtx: persistent ctx updates from a new outcome ─────────────
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Mutates ctx to absorb persistent state from a new outcome:
|
|
453
|
+
* - active agreement/task ids on assignment, cleared on close
|
|
454
|
+
* - pending proposal id on proposal-made, cleared on resolution
|
|
455
|
+
* - delegated task ids accumulated on delegation, used to match later subtask events
|
|
456
|
+
*/
|
|
457
|
+
export function applyOutcomeToCtx(ctx: Ctx, outcome: Outcome): void {
|
|
458
|
+
ctx.lastOutcome = outcome;
|
|
459
|
+
|
|
460
|
+
switch (outcome.kind) {
|
|
461
|
+
case 'task-assigned': {
|
|
462
|
+
const t = outcome.task || ({} as TaskRef);
|
|
463
|
+
const tid = t.taskId || (t as unknown as Record<string, unknown>).id as string | undefined;
|
|
464
|
+
const agId = t.agreementId || t.agreement?.agreementId || ctx.pendingProposalAgreementId;
|
|
465
|
+
if (tid) ctx.activeTaskId = tid;
|
|
466
|
+
if (agId) ctx.activeAgreementId = agId;
|
|
467
|
+
ctx.pendingProposalAgreementId = null;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
case 'task-delegated': {
|
|
471
|
+
const t = outcome.task || ({} as TaskRef);
|
|
472
|
+
const tid = t.taskId;
|
|
473
|
+
const agId = t.agreementId || t.agreement?.agreementId;
|
|
474
|
+
if (tid && !ctx.delegatedTaskIds.includes(tid)) ctx.delegatedTaskIds.push(tid);
|
|
475
|
+
if (agId && !ctx.delegatedAgreementIds.includes(agId)) ctx.delegatedAgreementIds.push(agId);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
case 'proposal-made': {
|
|
479
|
+
const agId = outcome.proposal?.agreementId;
|
|
480
|
+
if (agId) ctx.pendingProposalAgreementId = agId;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
case 'proposal-resolved': {
|
|
484
|
+
// Approval of the agent's PRIMARY proposal (the one binding it to its
|
|
485
|
+
// counterparty — e.g. Ziggs ↔ user) promotes the pending id into
|
|
486
|
+
// activeAgreementId so transitions gated on activeAgreementId fire.
|
|
487
|
+
// We only do this when no agreement is currently active, so that
|
|
488
|
+
// approvals of CHILD sub-agreements (e.g. coffee-agent approving the
|
|
489
|
+
// Ziggs-issued subcontract) don't overwrite the root the primary agent
|
|
490
|
+
// is bound to. Sub-agreement bookkeeping lives in delegatedAgreementIds
|
|
491
|
+
// (recorded on task-delegated) — not here.
|
|
492
|
+
if (outcome.action === 'approve' && !ctx.activeAgreementId) {
|
|
493
|
+
const agId = outcome.proposal?.agreementId || ctx.pendingProposalAgreementId;
|
|
494
|
+
if (agId) ctx.activeAgreementId = agId;
|
|
495
|
+
}
|
|
496
|
+
ctx.pendingProposalAgreementId = null;
|
|
497
|
+
return;
|
|
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
|
+
}
|
|
507
|
+
case 'task-closed': {
|
|
508
|
+
ctx.activeAgreementId = null;
|
|
509
|
+
ctx.activeTaskId = null;
|
|
510
|
+
ctx.pendingProposalAgreementId = null;
|
|
511
|
+
ctx.delegatedTaskIds = [];
|
|
512
|
+
ctx.delegatedAgreementIds = [];
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
default:
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Action, Transition, ThinkingState } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Defaults that workflow authors spread into thinking states explicitly.
|
|
5
|
+
* No auto-injection — what you read in the file is what runs.
|
|
6
|
+
*
|
|
7
|
+
* const dflt = thinkingDefaults({ initial: 'idle' });
|
|
8
|
+
*
|
|
9
|
+
* listening: {
|
|
10
|
+
* kind: 'thinking',
|
|
11
|
+
* ...dflt,
|
|
12
|
+
* prompt: { ... },
|
|
13
|
+
* actions: { ...dflt.actions, greetUser: { ... } },
|
|
14
|
+
* transitions: [
|
|
15
|
+
* { to: 'executing', when: o => o.kind === 'task-assigned' },
|
|
16
|
+
* ...dflt.transitions,
|
|
17
|
+
* ],
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
export interface ThinkingDefaults {
|
|
21
|
+
actions: Record<string, Action>;
|
|
22
|
+
transitions: Transition[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const DEFAULT_WAIT_PROMPT = Object.freeze({
|
|
26
|
+
instruction: 'Do nothing - wait for the next event.',
|
|
27
|
+
when: 'No action is needed, you are waiting for external input, or you should pause without repeating yourself.',
|
|
28
|
+
format: '{"thought": "...", "action": "wait"}',
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export function thinkingDefaults(opts: { initial: string }): ThinkingDefaults {
|
|
32
|
+
const { initial } = opts;
|
|
33
|
+
return {
|
|
34
|
+
actions: {
|
|
35
|
+
wait: {
|
|
36
|
+
prompt: { ...DEFAULT_WAIT_PROMPT },
|
|
37
|
+
produces: 'wait',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
transitions: [
|
|
41
|
+
{ to: initial, when: o => o.kind === 'wait' },
|
|
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' },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Convenience builder for a `thinking` state that always carries the defaults.
|
|
52
|
+
* Equivalent to spreading manually but the spread points are in one place.
|
|
53
|
+
*
|
|
54
|
+
* defineThinkingState({ initial: 'idle' }, {
|
|
55
|
+
* prompt: { ... },
|
|
56
|
+
* actions: { greetUser: { ... } },
|
|
57
|
+
* transitions: [ { to: 'executing', when: o => o.kind === 'task-assigned' } ],
|
|
58
|
+
* })
|
|
59
|
+
*/
|
|
60
|
+
export function defineThinkingState(
|
|
61
|
+
opts: { initial: string },
|
|
62
|
+
state: Omit<ThinkingState, 'kind' | 'actions' | 'transitions'> & {
|
|
63
|
+
actions: Record<string, Action>;
|
|
64
|
+
transitions: Transition[];
|
|
65
|
+
}
|
|
66
|
+
): ThinkingState {
|
|
67
|
+
const dflt = thinkingDefaults(opts);
|
|
68
|
+
return {
|
|
69
|
+
kind: 'thinking',
|
|
70
|
+
prompt: state.prompt,
|
|
71
|
+
allowNoWait: state.allowNoWait,
|
|
72
|
+
actions: { ...dflt.actions, ...state.actions },
|
|
73
|
+
transitions: [...state.transitions, ...dflt.transitions],
|
|
74
|
+
};
|
|
75
|
+
}
|