@ziggs-ai/agent-sdk 0.1.10 → 0.1.12
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/package.json +6 -3
- package/src/AgentHost.ts +347 -28
- package/src/adapters/AnthropicAdapter.ts +238 -0
- package/src/adapters/McpAdapter.ts +391 -15
- package/src/adapters/OpenAIAdapter.ts +20 -8
- package/src/adapters/index.ts +1 -0
- package/src/agent/Agent.ts +13 -2
- package/src/agent/ClaudeSdkAgent.ts +401 -0
- package/src/agent/claudeCognition.ts +139 -0
- package/src/agent/claudeSdkProtocol.ts +27 -0
- package/src/agent/claudeSdkTools.ts +81 -0
- package/src/agent/hostedAgentConnection.ts +41 -0
- package/src/agent/toolManagerToSdkMcp.ts +136 -0
- package/src/defineAgent.ts +22 -7
- package/src/index.ts +14 -5
- package/src/pricing/fleetTierByAgentId.gen.ts +53 -1
- package/src/runtime/AgentMachine.ts +143 -23
- package/src/runtime/PromptBuilder.ts +62 -6
- package/src/runtime/bootstrapCtx.ts +39 -0
- package/src/runtime/buildOutcome.ts +51 -6
- package/src/runtime/defaults.ts +12 -1
- package/src/runtime/deriveRespondsToProposals.ts +35 -0
- package/src/runtime/kickoff.ts +117 -0
- package/src/runtime/runMechanical.ts +106 -0
- package/src/runtime/runTurn.ts +236 -62
- package/src/runtime/validateWorkflow.ts +55 -15
- package/src/server/ConnectionPool.ts +56 -5
- package/src/server/InboxCatchUp.ts +37 -0
- package/src/server/StateTimers.ts +85 -0
- package/src/server/ZiggsEffectHandler.ts +198 -180
- package/src/server/agreements/AgreementService.ts +3 -3
- package/src/server/tasks/TaskService.ts +15 -3
- package/src/server/tools/tier1/protocolRunner.ts +221 -3
- package/src/server/tools/tier1/protocolTools.ts +19 -4
- package/src/types.ts +105 -29
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgreementService } from '../../agreements/AgreementService.js';
|
|
2
2
|
import type { TaskService } from '../../tasks/TaskService.js';
|
|
3
|
+
import type { ContextReadClient } from '@ziggs-ai/api-client';
|
|
3
4
|
import {
|
|
4
5
|
applyFleetCounterPricing,
|
|
5
6
|
applyFleetPricingForProtocol,
|
|
@@ -15,6 +16,7 @@ function normalizeState(state: string): string {
|
|
|
15
16
|
interface Services {
|
|
16
17
|
agreementService: AgreementService;
|
|
17
18
|
taskService: TaskService;
|
|
19
|
+
contextReadClient: ContextReadClient;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
interface ProtocolPayload {
|
|
@@ -22,6 +24,7 @@ interface ProtocolPayload {
|
|
|
22
24
|
agreementId?: string;
|
|
23
25
|
description?: string;
|
|
24
26
|
proposedTo?: string;
|
|
27
|
+
providerId?: string;
|
|
25
28
|
executorId?: string;
|
|
26
29
|
action?: 'approve' | 'reject';
|
|
27
30
|
price?: number;
|
|
@@ -32,6 +35,7 @@ interface ProtocolPayload {
|
|
|
32
35
|
parentAgreementId?: string;
|
|
33
36
|
parentTaskId?: string;
|
|
34
37
|
taskId?: string;
|
|
38
|
+
stepId?: string;
|
|
35
39
|
assigneeId?: string;
|
|
36
40
|
status?: string;
|
|
37
41
|
result?: unknown;
|
|
@@ -43,6 +47,39 @@ interface ProtocolPayload {
|
|
|
43
47
|
[key: string]: unknown;
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
// ZIG-793: the `plan` tool-schema is underspecified (`plan: {type:'object'}`,
|
|
51
|
+
// `planReviewTiming: 'string'`), so the LLM emits steps missing stepId/order and an
|
|
52
|
+
// out-of-enum planReviewTiming, which the backend DTO rejects. Normalize in-place here
|
|
53
|
+
// so every op (propose / subcontract / counter / task-spawn) sends a valid plan.
|
|
54
|
+
function normalizePlanInPlace(payload: ProtocolPayload): void {
|
|
55
|
+
const plan = payload.plan as { steps?: unknown } | undefined;
|
|
56
|
+
if (plan && Array.isArray(plan.steps)) {
|
|
57
|
+
plan.steps = plan.steps.map((s, i) => {
|
|
58
|
+
const step =
|
|
59
|
+
s && typeof s === 'object'
|
|
60
|
+
? (s as Record<string, unknown>)
|
|
61
|
+
: { description: String(s) };
|
|
62
|
+
return {
|
|
63
|
+
...step,
|
|
64
|
+
stepId:
|
|
65
|
+
typeof step.stepId === 'string' && step.stepId ? step.stepId : `step-${i + 1}`,
|
|
66
|
+
order: typeof step.order === 'number' ? step.order : i,
|
|
67
|
+
description:
|
|
68
|
+
typeof step.description === 'string'
|
|
69
|
+
? step.description
|
|
70
|
+
: String(step.description ?? ''),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
payload.planReviewTiming !== undefined &&
|
|
76
|
+
payload.planReviewTiming !== 'with_proposal' &&
|
|
77
|
+
payload.planReviewTiming !== 'before_execution'
|
|
78
|
+
) {
|
|
79
|
+
payload.planReviewTiming = 'with_proposal';
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
46
83
|
export async function dispatchProtocolOp(
|
|
47
84
|
services: Services,
|
|
48
85
|
payload: ProtocolPayload,
|
|
@@ -51,6 +88,7 @@ export async function dispatchProtocolOp(
|
|
|
51
88
|
actingAgentId: string | null = null,
|
|
52
89
|
): Promise<unknown> {
|
|
53
90
|
if (!payload?.operation) return null;
|
|
91
|
+
normalizePlanInPlace(payload);
|
|
54
92
|
const { operation } = payload;
|
|
55
93
|
const { agreementService, taskService } = services;
|
|
56
94
|
const actorId = actingAgentId?.trim() || null;
|
|
@@ -88,21 +126,100 @@ export async function dispatchProtocolOp(
|
|
|
88
126
|
case 'agreement-revoke':
|
|
89
127
|
if (!payload.agreementId) return null;
|
|
90
128
|
return agreementService.revoke(payload.agreementId);
|
|
91
|
-
case '
|
|
129
|
+
case 'agreement-fulfill':
|
|
130
|
+
if (!payload.agreementId) return null;
|
|
131
|
+
return agreementService.fulfill(payload.agreementId);
|
|
132
|
+
case 'task-spawn': {
|
|
92
133
|
if (!payload.agreementId || !payload.description) return null;
|
|
93
|
-
|
|
134
|
+
let spawnAgreementId = payload.agreementId;
|
|
135
|
+
const ags = await agreementsInChat(services, chatId);
|
|
136
|
+
// b2 (ZIG-832): if the assignee is not a party on the cited agreement,
|
|
137
|
+
// redirect the spawn to the ACTIVE sub-agreement (same root) where the
|
|
138
|
+
// assignee IS the provider — the orchestrator often cites the top-level
|
|
139
|
+
// hire, which the backend rejects with "assignee not a party".
|
|
140
|
+
// ZIG-858: also redirect when the cited agreement is no longer active —
|
|
141
|
+
// a count-bound sub fulfills after its quota (default 1), and the LLM
|
|
142
|
+
// keeps citing that now-terminal sub for the next spawn even when a
|
|
143
|
+
// fresh active sub for the same executor exists.
|
|
144
|
+
if (payload.assigneeId) {
|
|
145
|
+
const cited = ags.find((a) => a.agreementId === payload.agreementId);
|
|
146
|
+
if (cited && (!agIsParty(cited, payload.assigneeId) || !agIsActiveStatus(cited))) {
|
|
147
|
+
const root = cited.rootAgreementId || payload.agreementId;
|
|
148
|
+
const sub = ags.find(
|
|
149
|
+
(a) => agIsActiveStatus(a) && agIsProvider(a, payload.assigneeId as string) &&
|
|
150
|
+
(a.rootAgreementId === root || a.parentAgreementId === payload.agreementId ||
|
|
151
|
+
a.parentAgreementId === root),
|
|
152
|
+
);
|
|
153
|
+
if (sub?.agreementId) spawnAgreementId = sub.agreementId;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Safety guard (ZIG-832, B): a spawn against an agreement that is not active,
|
|
157
|
+
// or whose assignee is not a party on it, hard-400s at the backend AND makes
|
|
158
|
+
// the worker invent a "connection needed" message. That happens when the
|
|
159
|
+
// orchestrator spawns before its sub-agreement is subcontracted/approved.
|
|
160
|
+
// Skip GRACEFULLY instead — the sub-agreement's activation event will wake
|
|
161
|
+
// the orchestrator to spawn correctly on a later turn, no 400, no cascade.
|
|
162
|
+
const target = ags.find((a) => a.agreementId === spawnAgreementId);
|
|
163
|
+
// ZIG-858: a TERMINAL target is not the same as a not-yet-active one — no
|
|
164
|
+
// activation event will ever arrive, so the "wait for a later turn" skip
|
|
165
|
+
// below would stall the run. Tell the caller the actionable next step:
|
|
166
|
+
// subcontract a fresh sub for this executor, then spawn under it.
|
|
167
|
+
if (target && TERMINAL_STATUS.has(String(target.status))) {
|
|
168
|
+
return {
|
|
169
|
+
ok: false,
|
|
170
|
+
skipped: 'spawn-target-terminal',
|
|
171
|
+
reason: `agreement ${spawnAgreementId} is ${target.status} (terminal — its quota or term is exhausted). Subcontract a new sub-agreement to ${payload.assigneeId ?? 'the executor'}, then spawn this task under that new agreement.`,
|
|
172
|
+
agreementId: spawnAgreementId,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const notReady =
|
|
176
|
+
(target && !agIsActiveStatus(target)) ||
|
|
177
|
+
(!!payload.assigneeId && target && !agIsParty(target, payload.assigneeId));
|
|
178
|
+
if (notReady) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
skipped: 'spawn-target-not-ready',
|
|
182
|
+
reason: target && !agIsActiveStatus(target)
|
|
183
|
+
? `agreement ${spawnAgreementId} is ${target?.status ?? 'unknown'} (not active yet)`
|
|
184
|
+
: `assignee is not a party on ${spawnAgreementId} yet — subcontract to it first`,
|
|
185
|
+
agreementId: spawnAgreementId,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return taskService.spawnUnderAgreement(spawnAgreementId, {
|
|
94
189
|
description: payload.description,
|
|
95
190
|
parentTaskId: payload.parentTaskId,
|
|
96
191
|
plan: payload.plan,
|
|
97
192
|
planReviewTiming: payload.planReviewTiming,
|
|
98
193
|
requireMidWorkPlanAck: payload.requireMidWorkPlanAck,
|
|
99
194
|
assigneeId: payload.assigneeId,
|
|
195
|
+
inputArtifactIds: Array.isArray(payload.inputArtifactIds)
|
|
196
|
+
? (payload.inputArtifactIds as string[])
|
|
197
|
+
: undefined,
|
|
100
198
|
});
|
|
199
|
+
}
|
|
101
200
|
case 'task-update':
|
|
102
201
|
if (!payload.taskId || !payload.status) return null;
|
|
103
202
|
return taskService.updateState(payload.taskId, normalizeState(payload.status) as Parameters<typeof taskService.updateState>[1], payload.result);
|
|
203
|
+
case 'task-update-plan-step':
|
|
204
|
+
if (!payload.taskId) throw new Error('task-update-plan-step requires `taskId`');
|
|
205
|
+
if (!payload.stepId) throw new Error('task-update-plan-step requires `stepId`');
|
|
206
|
+
if (!payload.status) throw new Error('task-update-plan-step requires `status`');
|
|
207
|
+
// ZIG-793: a plan-step tick is cosmetic progress. Worker subtasks usually
|
|
208
|
+
// have NO plan (only the orchestrator's job task does), and the LLM may tick
|
|
209
|
+
// a stepId that isn't in the plan — the backend 400s on both. That must never
|
|
210
|
+
// hard-fail the tool → fail the task → wake-loop. Make it best-effort.
|
|
211
|
+
try {
|
|
212
|
+
return await taskService.updatePlanStep(
|
|
213
|
+
payload.taskId,
|
|
214
|
+
payload.stepId,
|
|
215
|
+
payload.status,
|
|
216
|
+
payload.result,
|
|
217
|
+
);
|
|
218
|
+
} catch {
|
|
219
|
+
return { ok: false, skipped: 'plan-step-not-updatable', stepId: payload.stepId };
|
|
220
|
+
}
|
|
104
221
|
default:
|
|
105
|
-
|
|
222
|
+
throw new Error(`Unknown protocol operation: ${operation}`);
|
|
106
223
|
}
|
|
107
224
|
}
|
|
108
225
|
|
|
@@ -115,11 +232,64 @@ async function proposeAgreement(
|
|
|
115
232
|
): Promise<unknown> {
|
|
116
233
|
if (!payload.description) throw new Error('agreement-propose requires `description`');
|
|
117
234
|
if (!payload.proposedTo) throw new Error('agreement-propose requires `proposedTo` (userId or agentId). To spawn a task under an existing agreement, use `task_spawn` instead.');
|
|
235
|
+
const isBroadcast = payload.proposedTo === 'everyone' || payload.proposedTo === 'org';
|
|
236
|
+
if (!isBroadcast && !payload.providerId) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
'agreement-propose requires `providerId` — name who does the work: your own id (you are offering to work) or the proposedTo id (you are commissioning the recipient).',
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
if (isBroadcast && payload.providerId) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
'agreement-propose forbids `providerId` on a broadcast — the claiming agent fills the open side.',
|
|
244
|
+
);
|
|
245
|
+
}
|
|
118
246
|
const priced = applyFleetPricingForProtocol('agreement-propose', payload, actingAgentId);
|
|
119
247
|
const { operation: _op, ...fields } = priced;
|
|
120
248
|
return services.agreementService.propose({ ...fields, chatId } as Parameters<typeof services.agreementService.propose>[0]);
|
|
121
249
|
}
|
|
122
250
|
|
|
251
|
+
// ── ZIG-832: deterministic sub-agreement resolution ─────────────────────────
|
|
252
|
+
// The orchestrator delegates one task per worker, but the LLM inconsistently
|
|
253
|
+
// (a) re-subcontracts the same worker (duplicate sub-agreements) and (b)
|
|
254
|
+
// task_spawns a worker under the TOP-LEVEL hire — where the worker is not a
|
|
255
|
+
// party — instead of under that worker's sub-agreement, which the backend
|
|
256
|
+
// rejects with 400 "assignee is not a party". Both are made deterministic here
|
|
257
|
+
// using the chat's existing agreements.
|
|
258
|
+
const TERMINAL_STATUS = new Set(['fulfilled', 'cancelled', 'refunded', 'rejected']);
|
|
259
|
+
type AgLike = Record<string, unknown> & {
|
|
260
|
+
agreementId?: string; status?: string; rootAgreementId?: string;
|
|
261
|
+
parentAgreementId?: string; parties?: Record<string, unknown>;
|
|
262
|
+
};
|
|
263
|
+
async function agreementsInChat(services: Services, chatId: string | null): Promise<AgLike[]> {
|
|
264
|
+
if (!chatId) return [];
|
|
265
|
+
try {
|
|
266
|
+
const snap = await services.contextReadClient.snapshot(chatId, { maxMessages: 1 });
|
|
267
|
+
const ags = snap.agreements;
|
|
268
|
+
return Array.isArray(ags) ? (ags as AgLike[]) : [];
|
|
269
|
+
} catch {
|
|
270
|
+
return [];
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function agIsProvider(a: AgLike, agentId: string): boolean {
|
|
274
|
+
const p = a.parties ?? {};
|
|
275
|
+
return p['provider'] === agentId || p['providerAgent'] === agentId || a['providerId'] === agentId;
|
|
276
|
+
}
|
|
277
|
+
function agIsParty(a: AgLike, agentId: string): boolean {
|
|
278
|
+
const p = a.parties ?? {};
|
|
279
|
+
return ['provider', 'providerAgent', 'payer', 'proposedTo', 'creator', 'creatorAgent']
|
|
280
|
+
.some((k) => p[k] === agentId);
|
|
281
|
+
}
|
|
282
|
+
function agActive(a: AgLike): boolean {
|
|
283
|
+
return !a.status || !TERMINAL_STATUS.has(String(a.status));
|
|
284
|
+
}
|
|
285
|
+
// Spawnable = actually ACTIVE. A freshly-subcontracted sub is `draft` until its
|
|
286
|
+
// counterparty auto-approves; spawning under a draft (or citing an agreement the
|
|
287
|
+
// assignee isn't a party on) 400s and cascades into a bogus worker "connection
|
|
288
|
+
// needed" message. We treat only 'active' as spawnable (ZIG-832).
|
|
289
|
+
function agIsActiveStatus(a: AgLike): boolean {
|
|
290
|
+
return String(a.status ?? '') === 'active';
|
|
291
|
+
}
|
|
292
|
+
|
|
123
293
|
async function subcontractAgreement(
|
|
124
294
|
services: Services,
|
|
125
295
|
payload: ProtocolPayload,
|
|
@@ -128,8 +298,56 @@ async function subcontractAgreement(
|
|
|
128
298
|
actingAgentId: string | null,
|
|
129
299
|
): Promise<unknown> {
|
|
130
300
|
if (!payload.description || !payload.executorId) return null;
|
|
301
|
+
// b1 (ZIG-832): idempotent subcontract — if a non-terminal sub-agreement for
|
|
302
|
+
// this executor already exists under the same parent, reuse it instead of
|
|
303
|
+
// creating a duplicate (which bloats the chain and blocks the fulfill cascade).
|
|
304
|
+
if (chatId) {
|
|
305
|
+
const parent = payload.parentAgreementId;
|
|
306
|
+
const existing = (await agreementsInChat(services, chatId)).find(
|
|
307
|
+
(a) => agActive(a) && agIsProvider(a, payload.executorId as string) &&
|
|
308
|
+
(!parent || a.parentAgreementId === parent || a.rootAgreementId === parent),
|
|
309
|
+
);
|
|
310
|
+
if (existing?.agreementId) return existing;
|
|
311
|
+
}
|
|
131
312
|
const priced = applyFleetPricingForProtocol('agreement-subcontract', payload, actingAgentId);
|
|
132
313
|
const { operation: _op, ...fields } = priced;
|
|
314
|
+
// ZIG-832: a subcontract must carry a working close condition, or it hangs
|
|
315
|
+
// 'active' forever — and because the backend only auto-fulfills a parent hire
|
|
316
|
+
// once every child sub-agreement is terminal (the fulfill cascade), one
|
|
317
|
+
// never-closing child leaves the whole engagement open. Deciding lifecycle is
|
|
318
|
+
// left to the model, which under load (context bloat, competing wakes) emits
|
|
319
|
+
// incomplete terms — e.g. time-bound with no expiresAt and no maxExecutions —
|
|
320
|
+
// that never reach fulfillment. Make the common single-delegated-task case
|
|
321
|
+
// deterministic: if the caller gave neither a real quota nor a real deadline,
|
|
322
|
+
// treat it as one count-bound execution so completing that task closes the
|
|
323
|
+
// sub — which then cascades the parent closed. A genuinely durable subcontract
|
|
324
|
+
// still opts out by setting maxExecutions>1 or a time-bound expiresAt.
|
|
325
|
+
const f = fields as { lifecycle?: string; maxExecutions?: number | string; expiresAt?: string };
|
|
326
|
+
// ZIG-858: repair incomplete terms instead of clobbering them. The model
|
|
327
|
+
// often emits a real quota or deadline but gets the envelope wrong — a
|
|
328
|
+
// numeric-string maxExecutions, or a quota/deadline with the lifecycle field
|
|
329
|
+
// missing or mismatched. The old check treated all of those as "no close
|
|
330
|
+
// condition" and forced count-bound/1, silently discarding the stated intent
|
|
331
|
+
// (e.g. maxExecutions=2 → 1, so the sub fulfills one task early and the next
|
|
332
|
+
// spawn under it fails).
|
|
333
|
+
if (typeof f.maxExecutions === 'string' && f.maxExecutions.trim() !== '' && Number.isFinite(Number(f.maxExecutions))) {
|
|
334
|
+
f.maxExecutions = Number(f.maxExecutions);
|
|
335
|
+
}
|
|
336
|
+
const hasQuota = typeof f.maxExecutions === 'number' && f.maxExecutions > 0;
|
|
337
|
+
const hasDeadline = typeof f.expiresAt === 'string' && f.expiresAt.length > 0;
|
|
338
|
+
if (f.lifecycle === 'count-bound' && hasQuota) {
|
|
339
|
+
// coherent as stated
|
|
340
|
+
} else if (f.lifecycle === 'time-bound' && hasDeadline) {
|
|
341
|
+
// coherent as stated
|
|
342
|
+
} else if (hasQuota) {
|
|
343
|
+
f.lifecycle = 'count-bound'; // quota given, lifecycle missing/mismatched — keep the quota
|
|
344
|
+
} else if (hasDeadline) {
|
|
345
|
+
f.lifecycle = 'time-bound'; // deadline given, lifecycle missing/mismatched — keep the deadline
|
|
346
|
+
} else {
|
|
347
|
+
// no usable close condition at all — the deterministic single-task default
|
|
348
|
+
f.lifecycle = 'count-bound';
|
|
349
|
+
f.maxExecutions = 1;
|
|
350
|
+
}
|
|
133
351
|
return services.agreementService.delegate({ ...fields, chatId } as Parameters<typeof services.agreementService.delegate>[0]);
|
|
134
352
|
}
|
|
135
353
|
|
|
@@ -2,10 +2,12 @@ import { defineTool, type ToolDefinition } from '../../../tools/defineTool.js';
|
|
|
2
2
|
import { dispatchProtocolOp } from './protocolRunner.js';
|
|
3
3
|
import type { AgreementService } from '../../agreements/AgreementService.js';
|
|
4
4
|
import type { TaskService } from '../../tasks/TaskService.js';
|
|
5
|
+
import type { ContextReadClient } from '@ziggs-ai/api-client';
|
|
5
6
|
|
|
6
7
|
interface ToolContext extends Record<string, unknown> {
|
|
7
8
|
taskService: TaskService;
|
|
8
9
|
agreementService: AgreementService;
|
|
10
|
+
contextReadClient: ContextReadClient;
|
|
9
11
|
chatId?: string | null;
|
|
10
12
|
agents?: unknown[];
|
|
11
13
|
/** Acting agent (ZIG-368 default pricing). */
|
|
@@ -17,8 +19,13 @@ function handlerFor(operation: string) {
|
|
|
17
19
|
const c = ctx as ToolContext;
|
|
18
20
|
if (!c.taskService) throw new Error('taskService missing from tool context (expected when running under Agent)');
|
|
19
21
|
if (!c.agreementService) throw new Error('agreementService missing from tool context (expected when running under Agent)');
|
|
22
|
+
if (!c.contextReadClient) throw new Error('contextReadClient missing from tool context (expected when running under Agent)');
|
|
20
23
|
const result = await dispatchProtocolOp(
|
|
21
|
-
{
|
|
24
|
+
{
|
|
25
|
+
taskService: c.taskService,
|
|
26
|
+
agreementService: c.agreementService,
|
|
27
|
+
contextReadClient: c.contextReadClient,
|
|
28
|
+
},
|
|
22
29
|
{ operation, ...args },
|
|
23
30
|
c.chatId ?? null,
|
|
24
31
|
c.agents ?? [],
|
|
@@ -31,9 +38,9 @@ function handlerFor(operation: string) {
|
|
|
31
38
|
|
|
32
39
|
export const agreementProposeTool: ToolDefinition = defineTool(
|
|
33
40
|
'agreement_propose',
|
|
34
|
-
{ description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service when omitted.' }, parentAgreementId: 'string', parentTaskId: 'string',
|
|
41
|
+
{ description: { type: 'string', required: true }, proposedTo: { type: 'string', required: true }, engagementKind: { type: 'string', enum: ['hire', 'service'], description: 'Contract type. Defaults to service when omitted.' }, parentAgreementId: 'string', parentTaskId: 'string', providerId: { type: 'string', description: 'Who does the work — required on direct proposals, forbidden on broadcasts (the claiming agent fills the open side). Pass your own id when YOU are offering to do the work, or the proposedTo id when you are commissioning the RECIPIENT. Naming any other third party requires that party to have a matching published offer. The payer is always derived as the non-providing side.' }, price: 'number', lifecycle: 'string', expiresAt: 'string', maxExecutions: 'number', agreementDescription: 'string', plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
|
|
35
42
|
handlerFor('agreement-propose'),
|
|
36
|
-
{ description: 'Propose a contract to a user, agent, or as an open broadcast.
|
|
43
|
+
{ description: 'Propose a contract to a user, agent, or as an open broadcast. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay"). Required on direct proposals; omit it only for a broadcast — proposedTo="everyone" (fully public — any agent may claim) or proposedTo="org" (scoped to your org). engagementKind defaults to service.', isAgreementCreation: true },
|
|
37
44
|
);
|
|
38
45
|
|
|
39
46
|
export const agreementSubcontractTool: ToolDefinition = defineTool(
|
|
@@ -78,6 +85,13 @@ export const agreementRevokeTool: ToolDefinition = defineTool(
|
|
|
78
85
|
{ description: 'Revoke a pending proposal you created so it can no longer be approved. Use when the user has clearly moved on to a different request before responding to the previous proposal card.' },
|
|
79
86
|
);
|
|
80
87
|
|
|
88
|
+
export const agreementFulfillTool: ToolDefinition = defineTool(
|
|
89
|
+
'agreement_fulfill',
|
|
90
|
+
{ agreementId: { type: 'string', required: true } },
|
|
91
|
+
handlerFor('agreement-fulfill'),
|
|
92
|
+
{ description: 'Mark an agreement you provide as fulfilled (complete) once its work is delivered — closes the engagement so it no longer reads as in-progress. Use on your hire after the final deliverable is done and delivered to the human.' },
|
|
93
|
+
);
|
|
94
|
+
|
|
81
95
|
export const taskUpdatePlanStepTool: ToolDefinition = defineTool(
|
|
82
96
|
'task_update_plan_step',
|
|
83
97
|
{ taskId: { type: 'string', required: true }, stepId: { type: 'string', required: true }, status: { type: 'string', required: true }, result: 'string' },
|
|
@@ -87,7 +101,7 @@ export const taskUpdatePlanStepTool: ToolDefinition = defineTool(
|
|
|
87
101
|
|
|
88
102
|
export const taskSpawnTool: ToolDefinition = defineTool(
|
|
89
103
|
'task_spawn',
|
|
90
|
-
{ agreementId: { type: 'string', required: true }, description: { type: 'string', required: true }, parentTaskId: 'string', assigneeId: { type: 'string', description: 'Explicitly hand this task to one specific agent (e.g. a worker your orchestrator selected via agent search) instead of leaving it unassigned. Must be a party on the agreement (payer, provider, creator, or creator agent).' }, plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
|
|
104
|
+
{ agreementId: { type: 'string', required: true }, description: { type: 'string', required: true }, parentTaskId: 'string', assigneeId: { type: 'string', description: 'Explicitly hand this task to one specific agent (e.g. a worker your orchestrator selected via agent search) instead of leaving it unassigned. Must be a party on the agreement (payer, provider, creator, or creator agent).' }, inputArtifactIds: { type: 'array', items: { type: 'string' }, description: 'ZIG-734: prior-step output artifact handles for structured input links.' }, plan: { type: 'object' }, planReviewTiming: 'string', requireMidWorkPlanAck: 'boolean' },
|
|
91
105
|
handlerFor('task-spawn'),
|
|
92
106
|
{ description: 'Spawn a new task under an existing approved agreement. Use when the contract is already in place. Pass assigneeId to delegate the task to a specific agent rather than leaving it open.' },
|
|
93
107
|
);
|
|
@@ -95,5 +109,6 @@ export const taskSpawnTool: ToolDefinition = defineTool(
|
|
|
95
109
|
export const PROTOCOL_TOOLS: ToolDefinition[] = [
|
|
96
110
|
agreementProposeTool, agreementSubcontractTool,
|
|
97
111
|
agreementRespondTool, agreementCounterProposalTool, agreementCheckProposalTool, agreementRevokeTool,
|
|
112
|
+
agreementFulfillTool,
|
|
98
113
|
taskSpawnTool, taskUpdateTool, taskUpdatePlanStepTool,
|
|
99
114
|
];
|
package/src/types.ts
CHANGED
|
@@ -26,6 +26,10 @@ export interface TaskRef {
|
|
|
26
26
|
state?: string;
|
|
27
27
|
description?: string;
|
|
28
28
|
parentTaskId?: string;
|
|
29
|
+
executorId?: string;
|
|
30
|
+
assigneeId?: string | null;
|
|
31
|
+
/** Work product set by task_update (safeJsonStringify'd string on the wire). */
|
|
32
|
+
result?: unknown;
|
|
29
33
|
proposal?: { status?: 'pending' | 'approved' | 'rejected' | 'cancelled' };
|
|
30
34
|
proposedToIsYou?: boolean;
|
|
31
35
|
providerIsYou?: boolean;
|
|
@@ -43,7 +47,7 @@ export interface ProposalRef {
|
|
|
43
47
|
export type OutcomeKind =
|
|
44
48
|
| 'enter'
|
|
45
49
|
| 'wait'
|
|
46
|
-
| 'timeout'
|
|
50
|
+
| 'state-timeout'
|
|
47
51
|
| 'message-sent'
|
|
48
52
|
| 'message-received'
|
|
49
53
|
| 'task-assigned'
|
|
@@ -59,7 +63,7 @@ export type OutcomeKind =
|
|
|
59
63
|
export type Outcome =
|
|
60
64
|
| { kind: 'enter' }
|
|
61
65
|
| { kind: 'wait' }
|
|
62
|
-
| { kind: 'timeout' }
|
|
66
|
+
| { kind: 'state-timeout'; state: string; waitedMs: number }
|
|
63
67
|
| { kind: 'message-sent'; text?: string; receiverId?: string }
|
|
64
68
|
| { kind: 'message-received'; text?: string; senderId?: string; senderType?: string }
|
|
65
69
|
| { kind: 'task-assigned'; task: TaskRef }
|
|
@@ -67,7 +71,20 @@ export type Outcome =
|
|
|
67
71
|
| { kind: 'task-closed'; status: 'completed' | 'failed'; result?: unknown }
|
|
68
72
|
| { kind: 'subtask-finished'; status: 'completed' | 'failed'; task: TaskRef; result?: unknown }
|
|
69
73
|
| { kind: 'proposal-made'; proposal: ProposalRef }
|
|
70
|
-
| {
|
|
74
|
+
| {
|
|
75
|
+
kind: 'proposal-resolved';
|
|
76
|
+
action: 'approve' | 'reject';
|
|
77
|
+
proposal: ProposalRef;
|
|
78
|
+
/**
|
|
79
|
+
* True when THIS agent made the proposal being resolved. Computed in
|
|
80
|
+
* AgentMachine._handleFrame by matching proposal.agreementId against
|
|
81
|
+
* ctx.pendingProposalAgreementId BEFORE applyOutcomeToCtx clears it —
|
|
82
|
+
* wire frames carry no trustworthy proposer field (agreement_lifecycle
|
|
83
|
+
* drops `parties` at ingress, and parties.creator is a principal id,
|
|
84
|
+
* not an agentId). Drives the injected agreement-kickoff default.
|
|
85
|
+
*/
|
|
86
|
+
proposedByMe: boolean;
|
|
87
|
+
}
|
|
71
88
|
| { kind: 'tool-result'; tool: string; result: unknown }
|
|
72
89
|
| {
|
|
73
90
|
kind: 'error';
|
|
@@ -109,11 +126,26 @@ export interface ToolResultEntry {
|
|
|
109
126
|
error?: string;
|
|
110
127
|
}
|
|
111
128
|
|
|
129
|
+
export interface PendingSubtask {
|
|
130
|
+
/** Executor the task was placed with; null only when the spawn result carried
|
|
131
|
+
* no executor identity (open/broadcast agreement task not yet claimed). */
|
|
132
|
+
agentId: string | null;
|
|
133
|
+
spawnedAt: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface CollectedSubtaskResult {
|
|
137
|
+
taskId: string;
|
|
138
|
+
agentId: string | null;
|
|
139
|
+
status: 'completed' | 'failed';
|
|
140
|
+
/** task.result from the finishing event; null when the event carried none. */
|
|
141
|
+
result: unknown;
|
|
142
|
+
}
|
|
143
|
+
|
|
112
144
|
/**
|
|
113
145
|
* Per-lane runtime state. Outcome is "what just happened"; Ctx is "what I know."
|
|
114
146
|
*
|
|
115
147
|
* Persistent identifiers (activeAgreementId, pendingProposalAgreementId,
|
|
116
|
-
*
|
|
148
|
+
* pendingSubtasks) survive across outcomes — they're the memory of which
|
|
117
149
|
* agreement the agent is currently bound to and which subtasks it's awaiting.
|
|
118
150
|
*/
|
|
119
151
|
export interface Ctx {
|
|
@@ -124,7 +156,10 @@ export interface Ctx {
|
|
|
124
156
|
activeTaskId: string | null;
|
|
125
157
|
pendingProposalAgreementId: string | null;
|
|
126
158
|
delegatedAgreementIds: string[];
|
|
127
|
-
|
|
159
|
+
/** Durable barrier for orchestration: every spawned-but-unfinished subtask, keyed by taskId. */
|
|
160
|
+
pendingSubtasks: Record<string, PendingSubtask>;
|
|
161
|
+
/** Every finished subtask of the current engagement, arrival order. Synthesis prompts render ALL of these. */
|
|
162
|
+
collectedResults: CollectedSubtaskResult[];
|
|
128
163
|
|
|
129
164
|
// Per-turn working memory
|
|
130
165
|
toolResults: ToolResultEntry[];
|
|
@@ -132,6 +167,17 @@ export interface Ctx {
|
|
|
132
167
|
// Last event/turn that caused this entry
|
|
133
168
|
lastOutcome: Outcome;
|
|
134
169
|
|
|
170
|
+
/** Claude Agent SDK session id for resume across ticks (claude-sdk cognition only). */
|
|
171
|
+
claudeSessionId?: string;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Recent wake timestamps (epoch ms) for this lane (claude-sdk cognition
|
|
175
|
+
* only). Backstop circuit breaker: if a lane wakes faster than any real
|
|
176
|
+
* traffic could drive it, the brain skips the LLM instead of burning the
|
|
177
|
+
* fleet key on a wake storm.
|
|
178
|
+
*/
|
|
179
|
+
recentWakes?: number[];
|
|
180
|
+
|
|
135
181
|
// Server-provided context (history, agreements, agents, users) is fetched
|
|
136
182
|
// on demand inside runTurn; not persisted on Ctx.
|
|
137
183
|
}
|
|
@@ -182,6 +228,17 @@ export interface Transition {
|
|
|
182
228
|
|
|
183
229
|
export interface ParkedState {
|
|
184
230
|
kind: 'parked';
|
|
231
|
+
/**
|
|
232
|
+
* Liveness timer. When the machine settles in this state and no event
|
|
233
|
+
* arrives on the lane within timeoutMs, the host wakes the lane with a
|
|
234
|
+
* { kind: 'state-timeout', state, waitedMs } outcome so a transition can
|
|
235
|
+
* route to a recovery/nudge state. If no transition matches, the machine
|
|
236
|
+
* parks loudly (runtimeLog.error) and the timer is NOT re-armed. Countdown
|
|
237
|
+
* starts when the tick settles and resets on any event for the lane.
|
|
238
|
+
* In-memory only: lost on process restart; re-armed by the next real event
|
|
239
|
+
* that ticks the lane.
|
|
240
|
+
*/
|
|
241
|
+
timeoutMs?: number;
|
|
185
242
|
transitions: Transition[];
|
|
186
243
|
}
|
|
187
244
|
|
|
@@ -198,7 +255,20 @@ export interface ThinkingState {
|
|
|
198
255
|
allowNoWait?: boolean;
|
|
199
256
|
}
|
|
200
257
|
|
|
201
|
-
|
|
258
|
+
/**
|
|
259
|
+
* ZIG-764: fixed tool invoke with no LLM turn. Entered after transitions
|
|
260
|
+
* (e.g. task-assigned → executing); runs `tool` once and emits `produces`.
|
|
261
|
+
*/
|
|
262
|
+
export interface MechanicalState {
|
|
263
|
+
kind: 'mechanical';
|
|
264
|
+
tool: string;
|
|
265
|
+
args?: Record<string, unknown>;
|
|
266
|
+
buildArgs?: (ctx: Ctx, outcome: Outcome) => Record<string, unknown>;
|
|
267
|
+
produces: Produces;
|
|
268
|
+
transitions: Transition[];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type State = ParkedState | ThinkingState | MechanicalState;
|
|
202
272
|
|
|
203
273
|
export interface Workflow {
|
|
204
274
|
id: string;
|
|
@@ -254,16 +324,20 @@ export interface ContextSnapshot {
|
|
|
254
324
|
timelineSummary?: string | null;
|
|
255
325
|
latestSequence?: string | null;
|
|
256
326
|
unavailable?: boolean;
|
|
327
|
+
/**
|
|
328
|
+
* The session's chat no longer resolves on the backend (messages read
|
|
329
|
+
* 404'd) — deleted or never visible to this agent. Message-triggered wakes
|
|
330
|
+
* check this and skip the LLM instead of reasoning over a fabricated empty
|
|
331
|
+
* history. Agreement-keyed lanes also set it (their sessionId is not a
|
|
332
|
+
* chat), so it only gates message wakes, never task lanes.
|
|
333
|
+
*/
|
|
334
|
+
chatMissing?: boolean;
|
|
257
335
|
/** Index signature so PromptBuilder's `ServerContext` can accept it directly. */
|
|
258
336
|
[key: string]: unknown;
|
|
259
337
|
}
|
|
260
338
|
|
|
261
339
|
export interface ReadContextOpts {
|
|
262
340
|
maxMessages?: number;
|
|
263
|
-
maxCharacters?: number;
|
|
264
|
-
maxTaskHistory?: number;
|
|
265
|
-
summarize?: boolean;
|
|
266
|
-
sinceSequence?: string;
|
|
267
341
|
}
|
|
268
342
|
|
|
269
343
|
/**
|
|
@@ -277,6 +351,12 @@ export interface ToolCallResult {
|
|
|
277
351
|
result?: unknown;
|
|
278
352
|
error?: string;
|
|
279
353
|
events?: unknown[];
|
|
354
|
+
/**
|
|
355
|
+
* Summed token usage of any nested LLM call the tool made (e.g. llmTool).
|
|
356
|
+
* Threaded up so the turn's authoritative token count (machine.tokens ->
|
|
357
|
+
* result.metrics) includes tool-internal spend rather than leaking it (ZIG-813).
|
|
358
|
+
*/
|
|
359
|
+
usage?: LlmResponse['usage'];
|
|
280
360
|
}
|
|
281
361
|
|
|
282
362
|
export interface RecordedEntry {
|
|
@@ -293,13 +373,7 @@ export type EffectKind =
|
|
|
293
373
|
| 'stream-text'
|
|
294
374
|
| 'tool-call'
|
|
295
375
|
| 'send-message'
|
|
296
|
-
| 'record-event'
|
|
297
|
-
| 'get-scope'
|
|
298
|
-
| 'list-messages'
|
|
299
|
-
| 'list-artifacts'
|
|
300
|
-
| 'list-task-history';
|
|
301
|
-
|
|
302
|
-
export interface ScopeRef { kind: 'chat' | 'agreement' | 'task' | 'counterparty'; id: string }
|
|
376
|
+
| 'record-event';
|
|
303
377
|
|
|
304
378
|
export type Effect =
|
|
305
379
|
| { kind: 'read-context'; sessionId: string; opts?: ReadContextOpts }
|
|
@@ -330,17 +404,20 @@ export type Effect =
|
|
|
330
404
|
memory?: MemoryStore;
|
|
331
405
|
senderId?: string;
|
|
332
406
|
senderType?: string;
|
|
407
|
+
/**
|
|
408
|
+
* Agreement this tool call is acting under (ZIG-831). For a delegated
|
|
409
|
+
* per-user MCP call, `senderId` is the delegating agent, not the end-user
|
|
410
|
+
* who owns the connection; this lets the gateway resolve the delegation
|
|
411
|
+
* root's user. Sourced from the waking task event's `result.agreementId`.
|
|
412
|
+
*/
|
|
413
|
+
onBehalfOfAgreementId?: string;
|
|
333
414
|
/** Per-turn UUID (ZIG-440). */
|
|
334
415
|
runId?: string;
|
|
335
416
|
/** FSM state name (ZIG-440). */
|
|
336
417
|
stateId?: string;
|
|
337
418
|
}
|
|
338
419
|
| { kind: 'send-message'; sessionId: string; receiverId: string; text: string; messageId?: string }
|
|
339
|
-
| { kind: 'record-event'; sessionId: string; entry: RecordedEntry }
|
|
340
|
-
| { kind: 'get-scope'; via: ScopeRef }
|
|
341
|
-
| { kind: 'list-messages'; chatId: string; after?: string; limit?: number }
|
|
342
|
-
| { kind: 'list-artifacts'; chatId?: string; agreementId?: string; after?: string; limit?: number }
|
|
343
|
-
| { kind: 'list-task-history'; taskId: string; after?: string; limit?: number };
|
|
420
|
+
| { kind: 'record-event'; sessionId: string; entry: RecordedEntry };
|
|
344
421
|
|
|
345
422
|
/**
|
|
346
423
|
* Typed result map. With this shape, `await eff({ kind: 'read-context', ... })`
|
|
@@ -350,14 +427,10 @@ export type Effect =
|
|
|
350
427
|
export interface EffectResultMap {
|
|
351
428
|
'read-context': ContextSnapshot;
|
|
352
429
|
'llm-call': LlmResponse;
|
|
353
|
-
'stream-text': { text: string };
|
|
430
|
+
'stream-text': { text: string; usage?: LlmResponse['usage'] };
|
|
354
431
|
'tool-call': ToolCallResult;
|
|
355
432
|
'send-message': { ok: true };
|
|
356
433
|
'record-event': { ok: true };
|
|
357
|
-
'get-scope': unknown;
|
|
358
|
-
'list-messages': { messages: unknown[]; latestSequence: string | null };
|
|
359
|
-
'list-artifacts': { artifacts: unknown[]; latestSequence: string | null };
|
|
360
|
-
'list-task-history': { taskId: string; history: unknown[]; latestSequence: string | null };
|
|
361
434
|
}
|
|
362
435
|
|
|
363
436
|
export type EffectResult<K extends EffectKind> = EffectResultMap[K];
|
|
@@ -389,11 +462,14 @@ export interface DefineAgentInput {
|
|
|
389
462
|
|
|
390
463
|
/**
|
|
391
464
|
* Frozen output of defineAgent. Consumed directly by AgentHost / createAgent.
|
|
392
|
-
*
|
|
393
|
-
*
|
|
465
|
+
* defineAgent injects ONE default: the agreement-kickoff state `kickingOff`
|
|
466
|
+
* plus a last-position transition into it, added to states that don't already
|
|
467
|
+
* handle a proposer-side proposal-resolved/approve outcome (runtime/kickoff.ts).
|
|
468
|
+
* `wait` defaults remain the author's visible spread of `thinkingDefaults`.
|
|
394
469
|
*/
|
|
395
470
|
export interface AgentConfig {
|
|
396
471
|
openaiKey: string | undefined;
|
|
472
|
+
anthropicKey: string | undefined;
|
|
397
473
|
operatorKey?: string;
|
|
398
474
|
agentId: string;
|
|
399
475
|
description: string;
|