@ziggs-ai/agent-sdk 0.2.2 → 0.4.0
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/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/dist/AgentHost.d.ts +12 -20
- package/dist/AgentHost.js +68 -142
- package/dist/agent/Agent.js +4 -0
- package/dist/agent/ClaudeSdkAgent.js +13 -4
- package/dist/agent/claudeSdkProtocol.d.ts +8 -7
- package/dist/agent/claudeSdkProtocol.js +8 -7
- package/dist/agent/claudeSdkTools.d.ts +4 -2
- package/dist/agent/claudeSdkTools.js +9 -4
- package/dist/agent/toolManagerToSdkMcp.js +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +4 -2
- package/dist/ingress/normalizeIncoming.d.ts +23 -8
- package/dist/ingress/normalizeIncoming.js +21 -32
- package/dist/runtime/AgentMachine.d.ts +5 -2
- package/dist/runtime/AgentMachine.js +8 -2
- package/dist/runtime/PromptBuilder.js +2 -2
- package/dist/runtime/runMechanical.d.ts +3 -1
- package/dist/runtime/runMechanical.js +2 -2
- package/dist/runtime/runTurn.d.ts +3 -1
- package/dist/runtime/runTurn.js +27 -9
- package/dist/runtime/wakeWriteKeys.d.ts +25 -0
- package/dist/runtime/wakeWriteKeys.js +134 -0
- package/dist/server/ConnectionPool.d.ts +31 -9
- package/dist/server/ConnectionPool.js +21 -24
- package/dist/server/ZiggsEffectHandler.js +17 -12
- package/dist/server/createHealthServer.js +5 -5
- package/dist/server/inboxLoop.d.ts +50 -0
- package/dist/server/{InboxCatchUp.js → inboxLoop.js} +64 -81
- package/dist/server/runLauncher.d.ts +1 -1
- package/dist/server/runLauncher.js +20 -7
- package/dist/server/staleBacklogGate.d.ts +53 -0
- package/dist/server/staleBacklogGate.js +120 -0
- package/dist/server/tasks/TaskService.d.ts +2 -1
- package/dist/server/tasks/TaskService.js +5 -2
- package/dist/server/tools/tier1/protocolRunner.d.ts +2 -1
- package/dist/server/tools/tier1/protocolRunner.js +43 -9
- package/dist/server/tools/tier1/protocolTools.js +1 -1
- package/dist/server/tools/tier2/contextTools.js +1 -1
- package/dist/shared/operatorKeyClaims.d.ts +22 -0
- package/dist/shared/operatorKeyClaims.js +35 -0
- package/dist/types.d.ts +40 -0
- package/package.json +2 -2
- package/dist/server/InboxCatchUp.d.ts +0 -24
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Launcher startup backlog policy (ZIG-910).
|
|
3
|
+
*
|
|
4
|
+
* "Work waits" has no statute of limitations on the backend — the inbox never
|
|
5
|
+
* expires anything. But when a fleet comes up after a dark period, replaying
|
|
6
|
+
* every weeks-old actionable slice means a mass of LLM turns on stale work at
|
|
7
|
+
* real token cost. The consumer chooses what to pick up: this gate keeps the
|
|
8
|
+
* operator-poll lazy start from resurrecting agents whose newest actionable
|
|
9
|
+
* item is older than a cutoff. Skips are never silent — one loud aggregated
|
|
10
|
+
* summary line announces what was left down and how to opt back in.
|
|
11
|
+
*
|
|
12
|
+
* Env:
|
|
13
|
+
* FLEET_REPLAY_MAX_AGE cutoff, e.g. "3d" | "12h" | "30m" | bare number =
|
|
14
|
+
* days (default "3d")
|
|
15
|
+
* FLEET_REPLAY_STALE=1 replay everything (disable the gate)
|
|
16
|
+
*
|
|
17
|
+
* Scope is the launcher only. The backend inbox is untouched, explicit
|
|
18
|
+
* `pool.startAgent()` / preStart are untouched, and an agent with ANY fresh item
|
|
19
|
+
* starts normally (its own inbox loop then drains the stale backlog too).
|
|
20
|
+
*/
|
|
21
|
+
import type { InboxEnvelope, InboxOperatorAgentEntry } from '@ziggs-ai/api-client';
|
|
22
|
+
export declare const DEFAULT_REPLAY_MAX_AGE_MS: number;
|
|
23
|
+
export interface ReplayPolicy {
|
|
24
|
+
/** Skip slices whose newest actionable item is older than this. */
|
|
25
|
+
maxAgeMs: number;
|
|
26
|
+
/** FLEET_REPLAY_STALE=1 — replay everything, gate disabled. */
|
|
27
|
+
replayAll: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* "3d" | "12h" | "30m" | "45s"; a bare number means days (the knob's natural
|
|
31
|
+
* granularity). Returns null on garbage so the caller can warn and fall back.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseReplayMaxAge(raw: string): number | null;
|
|
34
|
+
export declare function resolveReplayPolicy(env?: NodeJS.ProcessEnv): ReplayPolicy;
|
|
35
|
+
/**
|
|
36
|
+
* Newest actionable timestamp across an agent's slice: scope activity,
|
|
37
|
+
* proposals, and connection/MCP requests. Null when nothing carries a
|
|
38
|
+
* timestamp (e.g. a joined-but-idle chat) — age unprovable, treated as fresh.
|
|
39
|
+
*/
|
|
40
|
+
export declare function newestActionableAt(inbox: InboxEnvelope): number | null;
|
|
41
|
+
export declare function formatAge(ms: number): string;
|
|
42
|
+
export interface StaleBacklogGateOptions extends ReplayPolicy {
|
|
43
|
+
label?: string;
|
|
44
|
+
now?: () => number;
|
|
45
|
+
log?: (line: string) => void;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the `shouldLazyStart` gate for the operator poll. Skipped agents
|
|
49
|
+
* resurface in every envelope (reading never acks), so announcements are
|
|
50
|
+
* per-agent-once: each poll pass that skips agents not yet announced emits one
|
|
51
|
+
* aggregated warn line covering them.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createStaleBacklogGate({ maxAgeMs, replayAll, label, now, log, }: StaleBacklogGateOptions): (entry: InboxOperatorAgentEntry) => boolean;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
2
|
+
export const DEFAULT_REPLAY_MAX_AGE_MS = 3 * 24 * 60 * 60 * 1000;
|
|
3
|
+
/**
|
|
4
|
+
* "3d" | "12h" | "30m" | "45s"; a bare number means days (the knob's natural
|
|
5
|
+
* granularity). Returns null on garbage so the caller can warn and fall back.
|
|
6
|
+
*/
|
|
7
|
+
export function parseReplayMaxAge(raw) {
|
|
8
|
+
const m = /^\s*(\d+(?:\.\d+)?)\s*([dhms]?)\s*$/i.exec(raw);
|
|
9
|
+
if (!m)
|
|
10
|
+
return null;
|
|
11
|
+
const value = Number(m[1]);
|
|
12
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
13
|
+
return null;
|
|
14
|
+
const unitMs = { d: 86_400_000, h: 3_600_000, m: 60_000, s: 1_000 }[(m[2] || 'd').toLowerCase()];
|
|
15
|
+
return value * unitMs;
|
|
16
|
+
}
|
|
17
|
+
export function resolveReplayPolicy(env = process.env) {
|
|
18
|
+
const replayAll = env.FLEET_REPLAY_STALE === '1' || env.FLEET_REPLAY_STALE === 'true';
|
|
19
|
+
const raw = env.FLEET_REPLAY_MAX_AGE?.trim();
|
|
20
|
+
let maxAgeMs = DEFAULT_REPLAY_MAX_AGE_MS;
|
|
21
|
+
if (raw) {
|
|
22
|
+
const parsed = parseReplayMaxAge(raw);
|
|
23
|
+
if (parsed == null) {
|
|
24
|
+
runtimeLog.warn('staleBacklogGate', `FLEET_REPLAY_MAX_AGE="${raw}" is not a duration (use e.g. "3d", "12h", "30m") — using default ${formatAge(DEFAULT_REPLAY_MAX_AGE_MS)}`);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
maxAgeMs = parsed;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { maxAgeMs, replayAll };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Newest actionable timestamp across an agent's slice: scope activity,
|
|
34
|
+
* proposals, and connection/MCP requests. Null when nothing carries a
|
|
35
|
+
* timestamp (e.g. a joined-but-idle chat) — age unprovable, treated as fresh.
|
|
36
|
+
*/
|
|
37
|
+
export function newestActionableAt(inbox) {
|
|
38
|
+
let newest = null;
|
|
39
|
+
const consider = (iso) => {
|
|
40
|
+
if (!iso)
|
|
41
|
+
return;
|
|
42
|
+
const t = Date.parse(iso);
|
|
43
|
+
if (!Number.isNaN(t) && (newest == null || t > newest))
|
|
44
|
+
newest = t;
|
|
45
|
+
};
|
|
46
|
+
for (const scope of inbox.scopes ?? [])
|
|
47
|
+
consider(scope.latestAt);
|
|
48
|
+
for (const p of inbox.proposalsAwaitingMe ?? [])
|
|
49
|
+
consider(p.proposedAt);
|
|
50
|
+
for (const r of inbox.connectionRequestsAwaitingMe ?? [])
|
|
51
|
+
consider(r.requestedAt);
|
|
52
|
+
for (const r of inbox.mcpServerRequestsAwaitingMe ?? [])
|
|
53
|
+
consider(r.requestedAt);
|
|
54
|
+
return newest;
|
|
55
|
+
}
|
|
56
|
+
/** Unread items in a slice, for the summary line. Counts are backend-capped. */
|
|
57
|
+
function countItems(inbox) {
|
|
58
|
+
let items = 0;
|
|
59
|
+
for (const scope of inbox.scopes ?? []) {
|
|
60
|
+
items += (scope.newMessages ?? 0) + (scope.newArtifacts ?? 0);
|
|
61
|
+
}
|
|
62
|
+
items += (inbox.proposalsAwaitingMe ?? []).length;
|
|
63
|
+
items += (inbox.connectionRequestsAwaitingMe ?? []).length;
|
|
64
|
+
items += (inbox.mcpServerRequestsAwaitingMe ?? []).length;
|
|
65
|
+
return items;
|
|
66
|
+
}
|
|
67
|
+
export function formatAge(ms) {
|
|
68
|
+
if (ms >= 86_400_000)
|
|
69
|
+
return `${trimNum(ms / 86_400_000)}d`;
|
|
70
|
+
if (ms >= 3_600_000)
|
|
71
|
+
return `${trimNum(ms / 3_600_000)}h`;
|
|
72
|
+
if (ms >= 60_000)
|
|
73
|
+
return `${trimNum(ms / 60_000)}m`;
|
|
74
|
+
return `${trimNum(ms / 1_000)}s`;
|
|
75
|
+
}
|
|
76
|
+
function trimNum(n) {
|
|
77
|
+
return Number(n.toFixed(1)).toString();
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Build the `shouldLazyStart` gate for the operator poll. Skipped agents
|
|
81
|
+
* resurface in every envelope (reading never acks), so announcements are
|
|
82
|
+
* per-agent-once: each poll pass that skips agents not yet announced emits one
|
|
83
|
+
* aggregated warn line covering them.
|
|
84
|
+
*/
|
|
85
|
+
export function createStaleBacklogGate({ maxAgeMs, replayAll, label = 'launcher', now = Date.now, log, }) {
|
|
86
|
+
const emit = log ?? ((line) => runtimeLog.warn(label, line));
|
|
87
|
+
const announced = new Set();
|
|
88
|
+
let pendingAgents = [];
|
|
89
|
+
let pendingItems = 0;
|
|
90
|
+
let flushScheduled = false;
|
|
91
|
+
// One envelope's entries are gated synchronously; a microtask-deferred
|
|
92
|
+
// flush turns a whole boot flood into a single summary line.
|
|
93
|
+
const flush = () => {
|
|
94
|
+
flushScheduled = false;
|
|
95
|
+
if (pendingAgents.length === 0)
|
|
96
|
+
return;
|
|
97
|
+
const preview = pendingAgents.slice(0, 5).join(', ');
|
|
98
|
+
const more = pendingAgents.length > 5 ? `, +${pendingAgents.length - 5} more` : '';
|
|
99
|
+
emit(`startup backlog policy: skipped ${pendingItems} stale item(s) across ${pendingAgents.length} agent(s) older than ${formatAge(maxAgeMs)} (${preview}${more}) — set FLEET_REPLAY_STALE=1 to replay them`);
|
|
100
|
+
pendingAgents = [];
|
|
101
|
+
pendingItems = 0;
|
|
102
|
+
};
|
|
103
|
+
return (entry) => {
|
|
104
|
+
if (replayAll)
|
|
105
|
+
return true;
|
|
106
|
+
const newest = newestActionableAt(entry.inbox);
|
|
107
|
+
if (newest == null || now() - newest <= maxAgeMs)
|
|
108
|
+
return true;
|
|
109
|
+
if (!announced.has(entry.agentId)) {
|
|
110
|
+
announced.add(entry.agentId);
|
|
111
|
+
pendingAgents.push(entry.agentId);
|
|
112
|
+
pendingItems += countItems(entry.inbox);
|
|
113
|
+
if (!flushScheduled) {
|
|
114
|
+
flushScheduled = true;
|
|
115
|
+
queueMicrotask(flush);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -15,8 +15,9 @@ export declare class TaskService {
|
|
|
15
15
|
requireMidWorkPlanAck?: boolean;
|
|
16
16
|
assigneeId?: string;
|
|
17
17
|
inputArtifactIds?: string[];
|
|
18
|
+
idempotencyKey?: string;
|
|
18
19
|
}): Promise<Task>;
|
|
19
|
-
updateState(taskId: string, state: TaskState, result?: unknown): Promise<Task>;
|
|
20
|
+
updateState(taskId: string, state: TaskState, result?: unknown, idempotencyKey?: string): Promise<Task>;
|
|
20
21
|
updatePlanStep(taskId: string, stepId: string, status: string, result?: unknown): Promise<Task>;
|
|
21
22
|
cancel(taskId: string): Promise<Task>;
|
|
22
23
|
report(taskId: string, message?: string): Promise<Task>;
|
|
@@ -57,9 +57,12 @@ export class TaskService {
|
|
|
57
57
|
throw error;
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
async updateState(taskId, state, result) {
|
|
60
|
+
async updateState(taskId, state, result, idempotencyKey) {
|
|
61
61
|
try {
|
|
62
|
-
const data =
|
|
62
|
+
const data = {
|
|
63
|
+
...(result !== undefined ? { result: safeJsonStringify(result) } : {}),
|
|
64
|
+
...(idempotencyKey ? { idempotencyKey } : {}),
|
|
65
|
+
};
|
|
63
66
|
const updated = await updateTaskState(taskId, state, data, this.creds);
|
|
64
67
|
runtimeLog.info('TaskService', `Task state: ${taskId} → ${state}`);
|
|
65
68
|
return updated;
|
|
@@ -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 { WakeWriteKeys } from '../../../types.js';
|
|
3
4
|
import type { ContextReadClient } from '@ziggs-ai/api-client';
|
|
4
5
|
interface Services {
|
|
5
6
|
agreementService: AgreementService;
|
|
@@ -35,5 +36,5 @@ interface ProtocolPayload {
|
|
|
35
36
|
})[];
|
|
36
37
|
[key: string]: unknown;
|
|
37
38
|
}
|
|
38
|
-
export declare function dispatchProtocolOp(services: Services, payload: ProtocolPayload, chatId: string | null, agents?: unknown[], actingAgentId?: string | null): Promise<unknown>;
|
|
39
|
+
export declare function dispatchProtocolOp(services: Services, payload: ProtocolPayload, chatId: string | null, agents?: unknown[], actingAgentId?: string | null, wakeKeys?: WakeWriteKeys | null): Promise<unknown>;
|
|
39
40
|
export {};
|
|
@@ -34,7 +34,7 @@ function normalizePlanInPlace(payload) {
|
|
|
34
34
|
payload.planReviewTiming = 'with_proposal';
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null) {
|
|
37
|
+
export async function dispatchProtocolOp(services, payload, chatId, agents = [], actingAgentId = null, wakeKeys = null) {
|
|
38
38
|
if (!payload?.operation)
|
|
39
39
|
return null;
|
|
40
40
|
normalizePlanInPlace(payload);
|
|
@@ -43,9 +43,9 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
43
43
|
const actorId = actingAgentId?.trim() || null;
|
|
44
44
|
switch (operation) {
|
|
45
45
|
case 'agreement-propose':
|
|
46
|
-
return proposeAgreement(services, payload, chatId, agents, actorId);
|
|
46
|
+
return proposeAgreement(services, payload, chatId, agents, actorId, wakeKeys);
|
|
47
47
|
case 'agreement-subcontract':
|
|
48
|
-
return subcontractAgreement(services, payload, chatId, agents, actorId);
|
|
48
|
+
return subcontractAgreement(services, payload, chatId, agents, actorId, wakeKeys);
|
|
49
49
|
case 'agreement-respond':
|
|
50
50
|
if (!payload.agreementId || !payload.action)
|
|
51
51
|
return null;
|
|
@@ -146,12 +146,27 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
146
146
|
inputArtifactIds: Array.isArray(payload.inputArtifactIds)
|
|
147
147
|
? payload.inputArtifactIds
|
|
148
148
|
: undefined,
|
|
149
|
+
// Wake-derived idempotency key (delivery law): a crash-replay re-issuing
|
|
150
|
+
// this spawn resolves to the already-created task server-side instead of
|
|
151
|
+
// creating a duplicate. Keyed on what is actually written (the resolved
|
|
152
|
+
// agreement, not the LLM-cited one), so multiple spawns in one wake stay
|
|
153
|
+
// distinct.
|
|
154
|
+
idempotencyKey: wakeKeys?.taskSpawnKey({
|
|
155
|
+
agreementId: spawnAgreementId,
|
|
156
|
+
description: payload.description,
|
|
157
|
+
assigneeId: payload.assigneeId,
|
|
158
|
+
parentTaskId: payload.parentTaskId,
|
|
159
|
+
}),
|
|
149
160
|
});
|
|
150
161
|
}
|
|
151
|
-
case 'task-update':
|
|
162
|
+
case 'task-update': {
|
|
152
163
|
if (!payload.taskId || !payload.status)
|
|
153
164
|
return null;
|
|
154
|
-
|
|
165
|
+
const state = normalizeState(payload.status);
|
|
166
|
+
// Wake-derived idempotency key: a crash-replay re-issuing this transition
|
|
167
|
+
// no-ops server-side instead of erroring on the already-terminal task.
|
|
168
|
+
return taskService.updateState(payload.taskId, state, payload.result, wakeKeys?.taskStateKey(payload.taskId, state));
|
|
169
|
+
}
|
|
155
170
|
case 'task-update-plan-step':
|
|
156
171
|
if (!payload.taskId)
|
|
157
172
|
throw new Error('task-update-plan-step requires `taskId`');
|
|
@@ -173,7 +188,7 @@ export async function dispatchProtocolOp(services, payload, chatId, agents = [],
|
|
|
173
188
|
throw new Error(`Unknown protocol operation: ${operation}`);
|
|
174
189
|
}
|
|
175
190
|
}
|
|
176
|
-
async function proposeAgreement(services, payload, chatId, _agents, actingAgentId) {
|
|
191
|
+
async function proposeAgreement(services, payload, chatId, _agents, actingAgentId, wakeKeys = null) {
|
|
177
192
|
if (!payload.description)
|
|
178
193
|
throw new Error('agreement-propose requires `description`');
|
|
179
194
|
if (!payload.proposedTo)
|
|
@@ -187,7 +202,17 @@ async function proposeAgreement(services, payload, chatId, _agents, actingAgentI
|
|
|
187
202
|
}
|
|
188
203
|
const priced = applyFleetPricingForProtocol('agreement-propose', payload, actingAgentId);
|
|
189
204
|
const { operation: _op, ...fields } = priced;
|
|
190
|
-
|
|
205
|
+
// Wake-derived idempotency key (delivery law): a crash-replay re-issuing this
|
|
206
|
+
// propose resolves to the already-created agreement server-side instead of
|
|
207
|
+
// minting a duplicate proposal. Keyed on what the proposal writes, so
|
|
208
|
+
// multiple proposals in one wake stay distinct.
|
|
209
|
+
const idempotencyKey = wakeKeys?.agreementProposeKey({
|
|
210
|
+
proposedTo: payload.proposedTo,
|
|
211
|
+
providerId: payload.providerId,
|
|
212
|
+
description: payload.description,
|
|
213
|
+
parentAgreementId: payload.parentAgreementId,
|
|
214
|
+
});
|
|
215
|
+
return services.agreementService.propose({ ...fields, chatId, idempotencyKey });
|
|
191
216
|
}
|
|
192
217
|
// ── ZIG-832: deterministic sub-agreement resolution ─────────────────────────
|
|
193
218
|
// The orchestrator delegates one task per worker, but the LLM inconsistently
|
|
@@ -228,7 +253,7 @@ function agActive(a) {
|
|
|
228
253
|
function agIsActiveStatus(a) {
|
|
229
254
|
return String(a.status ?? '') === 'active';
|
|
230
255
|
}
|
|
231
|
-
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId) {
|
|
256
|
+
async function subcontractAgreement(services, payload, chatId, _agents, actingAgentId, wakeKeys = null) {
|
|
232
257
|
if (!payload.description || !payload.executorId)
|
|
233
258
|
return null;
|
|
234
259
|
// b1 (ZIG-832): idempotent subcontract — if a non-terminal sub-agreement for
|
|
@@ -284,5 +309,14 @@ async function subcontractAgreement(services, payload, chatId, _agents, actingAg
|
|
|
284
309
|
f.lifecycle = 'count-bound';
|
|
285
310
|
f.maxExecutions = 1;
|
|
286
311
|
}
|
|
287
|
-
|
|
312
|
+
// Wake-derived idempotency key (delivery law): a crash-replay that slips past
|
|
313
|
+
// the b1 reuse check above (e.g. no chatId, or the sub not yet visible in the
|
|
314
|
+
// snapshot) resolves to the already-created sub-agreement server-side instead
|
|
315
|
+
// of minting a duplicate.
|
|
316
|
+
const idempotencyKey = wakeKeys?.agreementSubcontractKey({
|
|
317
|
+
executorId: payload.executorId,
|
|
318
|
+
description: payload.description,
|
|
319
|
+
parentAgreementId: payload.parentAgreementId ?? '',
|
|
320
|
+
});
|
|
321
|
+
return services.agreementService.delegate({ ...fields, chatId, idempotencyKey });
|
|
288
322
|
}
|
|
@@ -13,7 +13,7 @@ function handlerFor(operation) {
|
|
|
13
13
|
taskService: c.taskService,
|
|
14
14
|
agreementService: c.agreementService,
|
|
15
15
|
contextReadClient: c.contextReadClient,
|
|
16
|
-
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null);
|
|
16
|
+
}, { operation, ...args }, c.chatId ?? null, c.agents ?? [], typeof c.agentId === 'string' ? c.agentId : null, c.wakeKeys ?? null);
|
|
17
17
|
if (result == null)
|
|
18
18
|
throw new Error(`Operation ${operation} returned no result`);
|
|
19
19
|
return result;
|
|
@@ -129,7 +129,7 @@ export const contextDiscoverGrantableTool = defineTool('context_discover_grantab
|
|
|
129
129
|
const items = await new ContextDiscoveryClient(operatorKey, agentId).discoverGrantable();
|
|
130
130
|
return { count: items.length, items };
|
|
131
131
|
}, {
|
|
132
|
-
description: 'See what context EXISTS in your orgs that you CANNOT read yet — the inverse of list_grants. Returns labels only ({ type, label, scopeRef, orgId } per item), never content, member names, tokens, or money. Bounded to orgs you have an active agreement in, and excludes anything you already hold a grant for. Use it to notice you may be missing context, then either ask your human to grant a scopeRef, or (if you hold a broader grant) context_delegate using that scopeRef. Pair with list_grants (what you hold) and context_expand_reach (what a held grant covers).',
|
|
132
|
+
description: 'See what context EXISTS in your orgs that you CANNOT read yet — the inverse of list_grants. Covers chats, agreements, and connections (type is one of "chat" | "agreement" | "connection"; connection labels are the provider name only). Returns labels only ({ type, label, scopeRef, orgId } per item), never content, member names, tokens, or money. Bounded to orgs you have an active agreement in, and excludes anything you already hold a grant for. Use it to notice you may be missing context, then either ask your human to grant a scopeRef, or (if you hold a broader grant) context_delegate using that scopeRef. Pair with list_grants (what you hold) and context_expand_reach (what a held grant covers).',
|
|
133
133
|
});
|
|
134
134
|
export const CONTEXT_TOOLS = [
|
|
135
135
|
contextReadTool,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side peek at an operator key's JWT payload (signature NOT verified —
|
|
3
|
+
* identity hint only, the backend remains the authority). Used by the fleet
|
|
4
|
+
* launcher to warn when the key is agent-bound (ZIG-911): a bound key
|
|
5
|
+
* collapses the operator multiplex to its one agent and 403s every other
|
|
6
|
+
* per-agent loop, so a large fleet quietly watches a single agent.
|
|
7
|
+
*/
|
|
8
|
+
export interface OperatorKeyClaims {
|
|
9
|
+
type?: string;
|
|
10
|
+
keyId?: string;
|
|
11
|
+
ownerId?: string;
|
|
12
|
+
boundAgentId?: string | null;
|
|
13
|
+
exp?: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function decodeOperatorKeyClaims(token: string): OperatorKeyClaims | null;
|
|
16
|
+
/**
|
|
17
|
+
* The ZIG-911 startup check: warn (never refuse — a one-agent launcher on a
|
|
18
|
+
* bound key is legitimate) when the fleet's control key is bound to a single
|
|
19
|
+
* agent while more than one agent is registered. Returns the warning line, or
|
|
20
|
+
* null when the setup is coherent.
|
|
21
|
+
*/
|
|
22
|
+
export declare function boundKeyFleetWarning(operatorKey: string, registeredAgents: number): string | null;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function decodeOperatorKeyClaims(token) {
|
|
2
|
+
const parts = token.trim().split('.');
|
|
3
|
+
if (parts.length !== 3)
|
|
4
|
+
return null;
|
|
5
|
+
try {
|
|
6
|
+
const json = Buffer.from(parts[1], 'base64url').toString('utf8');
|
|
7
|
+
const payload = JSON.parse(json);
|
|
8
|
+
return {
|
|
9
|
+
type: payload.type,
|
|
10
|
+
keyId: payload.keyId,
|
|
11
|
+
ownerId: payload.ownerId,
|
|
12
|
+
boundAgentId: payload.boundAgentId ?? null,
|
|
13
|
+
exp: payload.exp,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The ZIG-911 startup check: warn (never refuse — a one-agent launcher on a
|
|
22
|
+
* bound key is legitimate) when the fleet's control key is bound to a single
|
|
23
|
+
* agent while more than one agent is registered. Returns the warning line, or
|
|
24
|
+
* null when the setup is coherent.
|
|
25
|
+
*/
|
|
26
|
+
export function boundKeyFleetWarning(operatorKey, registeredAgents) {
|
|
27
|
+
if (registeredAgents <= 1)
|
|
28
|
+
return null;
|
|
29
|
+
const bound = decodeOperatorKeyClaims(operatorKey)?.boundAgentId;
|
|
30
|
+
if (!bound)
|
|
31
|
+
return null;
|
|
32
|
+
return (`fleet key is bound to agent "${bound}": the operator multiplex covers 1 of ${registeredAgents} registered agents, ` +
|
|
33
|
+
`and per-agent inbox loops for the rest will 403 ("cannot impersonate"). ` +
|
|
34
|
+
`Use an unbound owner key with agents:impersonate to run a fleet.`);
|
|
35
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -346,6 +346,43 @@ export interface RecordedEntry {
|
|
|
346
346
|
text?: string;
|
|
347
347
|
[k: string]: unknown;
|
|
348
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Deterministic idempotency keys for one wake's writes (delivery law, item 2).
|
|
351
|
+
* Created once per tick from (laneKey, triggering event) — see
|
|
352
|
+
* `createWakeWriteKeys` — and threaded to every artifact / task-state write the
|
|
353
|
+
* wake performs, so a crash-replay of the same wake reproduces the same keys
|
|
354
|
+
* and the backend drops the duplicates. Never LLM-supplied, never random.
|
|
355
|
+
*/
|
|
356
|
+
export interface WakeWriteKeys {
|
|
357
|
+
/** Stable id of this wake, or null when its trigger has no durable delivery identity (timer/synthetic frames) — null means writes go keyless, as before. */
|
|
358
|
+
readonly wakeKey: string | null;
|
|
359
|
+
/** Key for the next artifact write of this wake. Draws a per-wake ordinal — call exactly once per write, at the call site. */
|
|
360
|
+
artifactKey(): string | undefined;
|
|
361
|
+
/** Key for a task state transition. Derived from (taskId, state), so it is stable regardless of tool-call order within the wake. */
|
|
362
|
+
taskStateKey(taskId: string, state: string): string | undefined;
|
|
363
|
+
/** Default messageId for the next chat-message send of this wake — the backend dedups on (chatId, messageId). Draws the same per-wake ordinal as artifactKey — call exactly once per send, at the call site. */
|
|
364
|
+
messageKey(): string | undefined;
|
|
365
|
+
/** Key for a task spawn. Derived from what the spawn writes (agreement, description, assignee, parent), so it is stable regardless of tool-call order within the wake. */
|
|
366
|
+
taskSpawnKey(fields: {
|
|
367
|
+
agreementId: string;
|
|
368
|
+
description: string;
|
|
369
|
+
assigneeId?: string;
|
|
370
|
+
parentTaskId?: string;
|
|
371
|
+
}): string | undefined;
|
|
372
|
+
/** Key for an agreement proposal (`agreement_propose`). Derived from what the proposal writes (proposedTo, provider, description, parent), so a crash-replay dedups the row instead of minting a duplicate proposal — and multiple proposals in one wake stay distinct. */
|
|
373
|
+
agreementProposeKey(fields: {
|
|
374
|
+
proposedTo: string;
|
|
375
|
+
providerId?: string;
|
|
376
|
+
description: string;
|
|
377
|
+
parentAgreementId?: string;
|
|
378
|
+
}): string | undefined;
|
|
379
|
+
/** Key for a sub-agreement (`agreement_subcontract`). Derived from what the subcontract writes (executor, description, parent), so a replay dedups the row. */
|
|
380
|
+
agreementSubcontractKey(fields: {
|
|
381
|
+
executorId: string;
|
|
382
|
+
description: string;
|
|
383
|
+
parentAgreementId: string;
|
|
384
|
+
}): string | undefined;
|
|
385
|
+
}
|
|
349
386
|
import type { MemoryStore } from './memory/MemoryStore.js';
|
|
350
387
|
export type EffectKind = 'read-context' | 'llm-call' | 'stream-text' | 'tool-call' | 'send-message' | 'record-event';
|
|
351
388
|
export type Effect = {
|
|
@@ -388,6 +425,8 @@ export type Effect = {
|
|
|
388
425
|
runId?: string;
|
|
389
426
|
/** FSM state name (ZIG-440). */
|
|
390
427
|
stateId?: string;
|
|
428
|
+
/** Replay-stable write keys for this wake — write-capable tools derive their idempotency key from here, never from the LLM. */
|
|
429
|
+
wakeKeys?: WakeWriteKeys;
|
|
391
430
|
} | {
|
|
392
431
|
kind: 'send-message';
|
|
393
432
|
sessionId: string;
|
|
@@ -398,6 +437,7 @@ export type Effect = {
|
|
|
398
437
|
kind: 'record-event';
|
|
399
438
|
sessionId: string;
|
|
400
439
|
entry: RecordedEntry;
|
|
440
|
+
idempotencyKey?: string;
|
|
401
441
|
};
|
|
402
442
|
/**
|
|
403
443
|
* Typed result map. With this shape, `await eff({ kind: 'read-context', ... })`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"@anthropic-ai/claude-agent-sdk": "^0.3.204",
|
|
27
27
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
28
28
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
29
|
-
"@ziggs-ai/api-client": "^0.
|
|
29
|
+
"@ziggs-ai/api-client": "^0.3.0",
|
|
30
30
|
"ajv": "^8.17.1",
|
|
31
31
|
"dotenv": "^17.2.3",
|
|
32
32
|
"openai": "^4.0.0",
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { type Creds } from '@ziggs-ai/api-client';
|
|
2
|
-
import { SeenMessages } from './SeenMessages.js';
|
|
3
|
-
export interface InboxCatchUpDeps {
|
|
4
|
-
creds: Creds;
|
|
5
|
-
ownAgentId: string;
|
|
6
|
-
baseUrl?: string;
|
|
7
|
-
seen: SeenMessages;
|
|
8
|
-
onMessage: (text: string, metadata: Record<string, unknown>) => Promise<void>;
|
|
9
|
-
onResourceEvent: (event: Record<string, unknown>) => Promise<void>;
|
|
10
|
-
label?: string;
|
|
11
|
-
}
|
|
12
|
-
export interface InboxCatchUpResult {
|
|
13
|
-
scopes: number;
|
|
14
|
-
messagesDelivered: number;
|
|
15
|
-
resourceEvents: number;
|
|
16
|
-
acked: number;
|
|
17
|
-
truncated: boolean;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Pull-based recovery after reconnect (ZIG-434 / ZIG-446 / ZIG-454).
|
|
21
|
-
* Inbox → read message deltas → deliver → ack. Push remains a hint;
|
|
22
|
-
* this path survives backend restarts that drop in-memory pending queues.
|
|
23
|
-
*/
|
|
24
|
-
export declare function runInboxCatchUp(deps: InboxCatchUpDeps): Promise<InboxCatchUpResult>;
|