@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
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ContextDiscoveryClient,
|
|
4
4
|
InboxClient,
|
|
5
5
|
getChatsForAgreement,
|
|
6
|
+
getAgreementStatus,
|
|
6
7
|
type InboxScopeEntry,
|
|
7
8
|
type InboxAck,
|
|
8
9
|
type Creds,
|
|
@@ -40,6 +41,22 @@ function warn(label: string, msg: string): void {
|
|
|
40
41
|
runtimeLog.warn(label, msg);
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
const TERMINAL_AGREEMENT_STATUSES = new Set(['fulfilled', 'cancelled', 'refunded']);
|
|
45
|
+
|
|
46
|
+
/** ZIG-832 P4: is this agreement finished (so its backlog must not be re-run)? */
|
|
47
|
+
async function isTerminalAgreement(agreementId: string, creds: Creds): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
const res = (await getAgreementStatus(agreementId, creds)) as
|
|
50
|
+
| { status?: string; agreement?: { status?: string } }
|
|
51
|
+
| undefined;
|
|
52
|
+
const status = res?.status ?? res?.agreement?.status;
|
|
53
|
+
return status != null && TERMINAL_AGREEMENT_STATUSES.has(status);
|
|
54
|
+
} catch {
|
|
55
|
+
// On lookup failure, don't skip — fall through to normal delivery.
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
43
60
|
/** Map Mongo message row → wire metadata for normalizeIncomingEvent. */
|
|
44
61
|
function rowToMetadata(row: WireRow, chatId: string): Record<string, unknown> {
|
|
45
62
|
const senderRaw = row.sender as { agentId?: string; id?: string; type?: string } | undefined;
|
|
@@ -182,6 +199,26 @@ export async function runInboxCatchUp(deps: InboxCatchUpDeps): Promise<InboxCatc
|
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
for (const scope of envelope.scopes) {
|
|
202
|
+
// ZIG-832 P4: a re-host runs catch-up from the server watermark and would
|
|
203
|
+
// re-deliver an already-finished agreement's backlog, re-running a completed
|
|
204
|
+
// pipeline and burning tokens. If this is an agreement scope that has reached
|
|
205
|
+
// a terminal status, ack it (so it drops out of future catch-ups) and skip
|
|
206
|
+
// re-delivery. Only agreement scopes — chat scopes may carry genuinely new
|
|
207
|
+
// conversation, so those are never skipped on status. (The deeper fix, acking
|
|
208
|
+
// the watermark on the live-push path so nothing re-delivers, is tracked
|
|
209
|
+
// separately.)
|
|
210
|
+
if (
|
|
211
|
+
scope.scope.kind === 'agreement' &&
|
|
212
|
+
(scope.newMessages > 0 || scope.newArtifacts > 0) &&
|
|
213
|
+
(await isTerminalAgreement(scope.scope.id, deps.creds))
|
|
214
|
+
) {
|
|
215
|
+
if (scope.latestAt) {
|
|
216
|
+
toAck.push({ kind: scope.scope.kind, id: scope.scope.id, upTo: scope.latestAt });
|
|
217
|
+
}
|
|
218
|
+
log(label, `scope agreement:${scope.scope.id} terminal — acked, not re-delivered`);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
185
222
|
const chatIds = await chatIdsForScope(scope, deps.creds, deps.baseUrl, discovery);
|
|
186
223
|
if (!chatIds.length && (scope.newMessages > 0 || scope.newArtifacts > 0)) {
|
|
187
224
|
warn(
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
2
|
+
|
|
3
|
+
export interface StateTimeoutEvent {
|
|
4
|
+
type: 'state-timeout';
|
|
5
|
+
state: string;
|
|
6
|
+
waitedMs: number;
|
|
7
|
+
epoch: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ArmedTimer {
|
|
11
|
+
epoch: number;
|
|
12
|
+
state: string;
|
|
13
|
+
timeoutMs: number;
|
|
14
|
+
armedAt: number;
|
|
15
|
+
handle: ReturnType<typeof setTimeout>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Per-lane liveness timers for parked states declaring timeoutMs. Owned by
|
|
20
|
+
* AgentHost. A firing timer enqueues a state-timeout frame on the SAME lane
|
|
21
|
+
* (caller-provided enqueue → EventQueue), so it serializes behind any real
|
|
22
|
+
* event and can never race one. Epochs make stale frames identifiable: a
|
|
23
|
+
* queued timeout frame whose epoch is not the lane's currently-armed epoch was
|
|
24
|
+
* superseded (a tick ran after it fired) and must be dropped.
|
|
25
|
+
*
|
|
26
|
+
* In-memory only: process restart loses all timers; each lane re-arms on its
|
|
27
|
+
* next real event (sessionStore persists { ctx, state } but not timers).
|
|
28
|
+
*/
|
|
29
|
+
export class StateTimers {
|
|
30
|
+
private _timers = new Map<string, ArmedTimer>();
|
|
31
|
+
private _epoch = 0;
|
|
32
|
+
private _enqueue: (event: StateTimeoutEvent, laneKey: string) => void;
|
|
33
|
+
|
|
34
|
+
constructor(enqueue: (event: StateTimeoutEvent, laneKey: string) => void) {
|
|
35
|
+
this._enqueue = enqueue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Re-arm is idempotent: any previously armed timer for the lane is cancelled first. */
|
|
39
|
+
arm(laneKey: string, state: string, timeoutMs: number): void {
|
|
40
|
+
this.clear(laneKey);
|
|
41
|
+
const epoch = ++this._epoch;
|
|
42
|
+
const handle = setTimeout(() => {
|
|
43
|
+
runtimeLog.debug('StateTimers', `fired laneKey=${laneKey} state=${state} waitedMs=${timeoutMs} epoch=${epoch}`);
|
|
44
|
+
this._enqueue({ type: 'state-timeout', state, waitedMs: timeoutMs, epoch }, laneKey);
|
|
45
|
+
}, timeoutMs);
|
|
46
|
+
(handle as unknown as { unref?: () => void }).unref?.();
|
|
47
|
+
this._timers.set(laneKey, { epoch, state, timeoutMs, armedAt: Date.now(), handle });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
clear(laneKey: string): void {
|
|
51
|
+
const t = this._timers.get(laneKey);
|
|
52
|
+
if (!t) return;
|
|
53
|
+
clearTimeout(t.handle);
|
|
54
|
+
this._timers.delete(laneKey);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
clearAll(): void {
|
|
58
|
+
for (const key of [...this._timers.keys()]) this.clear(key);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Drop stale state-timeout events from an incoming frame (single event or an
|
|
63
|
+
* EventQueue-coalesced { type: 'batch', events } frame). Returns null when
|
|
64
|
+
* nothing survives (pure stale-timeout frame — the lane's live timer, if any,
|
|
65
|
+
* must be left untouched by the caller in that case).
|
|
66
|
+
*/
|
|
67
|
+
stripStale(event: unknown, laneKey: string): unknown | null {
|
|
68
|
+
const pending = this._timers.get(laneKey);
|
|
69
|
+
const fresh = (ev: unknown): boolean => {
|
|
70
|
+
const e = ev as { type?: string; epoch?: number };
|
|
71
|
+
if (e?.type !== 'state-timeout') return true;
|
|
72
|
+
const ok = pending !== undefined && e.epoch === pending.epoch;
|
|
73
|
+
if (!ok) runtimeLog.debug('StateTimers', `dropping stale state-timeout laneKey=${laneKey} epoch=${e.epoch} armed=${pending?.epoch ?? 'none'}`);
|
|
74
|
+
return ok;
|
|
75
|
+
};
|
|
76
|
+
const b = event as { type?: string; events?: unknown[] };
|
|
77
|
+
if (b?.type === 'batch' && Array.isArray(b.events)) {
|
|
78
|
+
const kept = b.events.filter(fresh);
|
|
79
|
+
if (kept.length === 0) return null;
|
|
80
|
+
if (kept.length === b.events.length) return event;
|
|
81
|
+
return kept.length === 1 ? kept[0] : { type: 'batch', events: kept };
|
|
82
|
+
}
|
|
83
|
+
return fresh(event) ? event : null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ziggs-flavored EffectHandler.
|
|
3
3
|
*
|
|
4
|
-
* Translates the Agent's pure Effect requests into concrete calls
|
|
5
|
-
*
|
|
6
|
-
* taskService
|
|
7
|
-
* is the only place where Agent intent meets
|
|
4
|
+
* Translates the Agent's pure Effect requests into concrete backend calls:
|
|
5
|
+
* reads go through the grant-fenced ContextReadClient snapshot, writes through
|
|
6
|
+
* artifactsClient / taskService / agreementService / messageSender, and LLM
|
|
7
|
+
* turns through the adapter. This is the only place where Agent intent meets
|
|
8
|
+
* backend reality.
|
|
8
9
|
*
|
|
9
10
|
* Lives in agent-sdk for now alongside `Agent`; destined for a separate
|
|
10
11
|
* `@ziggs-ai/agent-server` package once the SDK split is complete.
|
|
@@ -18,15 +19,14 @@ import { runtimeLog } from '../shared/runtimeLog.js';
|
|
|
18
19
|
/** Minimal LLM adapter contract — OpenAIAdapter satisfies this. */
|
|
19
20
|
interface LlmAdapter {
|
|
20
21
|
chatMessages(messages: LlmMessage[], tools?: LlmToolSchema[], options?: Record<string, unknown>): Promise<LlmResponse>;
|
|
21
|
-
chatMessagesStream?(messages: LlmMessage[], onChunk: (text: string) => void): Promise<string>;
|
|
22
|
+
chatMessagesStream?(messages: LlmMessage[], onChunk: (text: string) => void): Promise<{ text: string; usage?: LlmResponse['usage'] }>;
|
|
22
23
|
}
|
|
23
24
|
import type { TaskService } from './tasks/TaskService.js';
|
|
24
25
|
import type { AgreementService } from './agreements/AgreementService.js';
|
|
25
26
|
import type { ToolManager } from '../tools/ToolManager.js';
|
|
26
27
|
import type {
|
|
27
|
-
MessagesClient,
|
|
28
28
|
ArtifactsClient,
|
|
29
|
-
|
|
29
|
+
ContextReadClient,
|
|
30
30
|
TelemetryClient,
|
|
31
31
|
} from '@ziggs-ai/api-client';
|
|
32
32
|
import {
|
|
@@ -37,10 +37,15 @@ import {
|
|
|
37
37
|
} from './telemetryIngest.js';
|
|
38
38
|
|
|
39
39
|
export interface ZiggsEffectDeps {
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
/**
|
|
41
|
+
* LLM adapter for `llm-call` / `stream-text`. Optional: the Claude brain
|
|
42
|
+
* drives its own model via `query()` and emits neither effect, so it wires
|
|
43
|
+
* no adapter. Those two cases throw if reached without one.
|
|
44
|
+
*/
|
|
45
|
+
llm?: LlmAdapter;
|
|
42
46
|
artifactsClient: ArtifactsClient;
|
|
43
|
-
|
|
47
|
+
/** Grant-fenced aggregate read backbone — sources the read-context snapshot. */
|
|
48
|
+
contextReadClient: ContextReadClient;
|
|
44
49
|
taskService: TaskService;
|
|
45
50
|
agreementService: AgreementService;
|
|
46
51
|
messageSender: (text: string, receiverId: string, sessionId: string, messageId?: string) => Promise<unknown>;
|
|
@@ -58,69 +63,47 @@ export interface ZiggsEffectDeps {
|
|
|
58
63
|
const CONTEXT_CACHE_MAX = 256;
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
|
-
* Compose a `ContextSnapshot` for a chatId
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
66
|
+
* Compose a `ContextSnapshot` for a chatId via the grant-fenced aggregate read
|
|
67
|
+
* (`GET /context/snapshot`, ZIG-775 follow-up). The backend assembles history +
|
|
68
|
+
* agreements + roster through the same `assertScopeAccess` + temporal fence as
|
|
69
|
+
* the per-type context reads, and annotates `*IsYou` / self server-side against
|
|
70
|
+
* the acting agent. This function only preserves the never-throw/degrade
|
|
71
|
+
* contract that the cognition code depends on — the shape is unchanged.
|
|
65
72
|
*/
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
};
|
|
75
|
-
};
|
|
73
|
+
const emptySnapshot = (agentId: string, chatMissing = false): ContextSnapshot => ({
|
|
74
|
+
history: [],
|
|
75
|
+
agreements: [],
|
|
76
|
+
users: [],
|
|
77
|
+
agents: [{ agentId, role: 'self', isYou: true }],
|
|
78
|
+
latestSequence: null,
|
|
79
|
+
...(chatMissing ? { chatMissing: true } : {}),
|
|
80
|
+
});
|
|
76
81
|
|
|
77
82
|
async function buildSnapshot(
|
|
78
83
|
deps: ZiggsEffectDeps,
|
|
79
84
|
chatId: string,
|
|
80
85
|
agentId: string,
|
|
81
|
-
opts: {
|
|
86
|
+
opts: { limit?: number } = {},
|
|
82
87
|
): Promise<ContextSnapshot> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
role: string;
|
|
103
|
-
principalType?: 'user' | 'agent';
|
|
104
|
-
}>;
|
|
105
|
-
|
|
106
|
-
const others = counterparties.filter((c) => c.id !== agentId);
|
|
107
|
-
const users = others
|
|
108
|
-
.filter((c) => c.principalType === 'user')
|
|
109
|
-
.map((c) => ({ userId: c.id, role: c.role }));
|
|
110
|
-
const agents = others
|
|
111
|
-
.filter((c) => c.principalType !== 'user') // 'agent' or missing → treat as agent
|
|
112
|
-
.map((c) => ({ agentId: c.id, role: c.role, isYou: false }));
|
|
113
|
-
agents.push({ agentId, role: 'self', isYou: true });
|
|
114
|
-
|
|
115
|
-
const agreements = (chatAgreements as Record<string, unknown>[]).map((ag) => annotateAgreement(ag, agentId));
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
history: msgs.messages as Record<string, unknown>[],
|
|
119
|
-
agreements,
|
|
120
|
-
agents,
|
|
121
|
-
users,
|
|
122
|
-
latestSequence: msgs.latestSequence,
|
|
123
|
-
};
|
|
88
|
+
try {
|
|
89
|
+
// Server returns 200 + chatMissing:true for a deleted chat, so its
|
|
90
|
+
// annotations (roster, *IsYou, self) are already applied — pass through.
|
|
91
|
+
const snap = await deps.contextReadClient.snapshot(chatId, {
|
|
92
|
+
maxMessages: opts.limit,
|
|
93
|
+
});
|
|
94
|
+
return snap as ContextSnapshot;
|
|
95
|
+
} catch (err: unknown) {
|
|
96
|
+
// Defensive: server normally signals a dead chat as 200 + chatMissing, but
|
|
97
|
+
// a raw 404 degrades the same way so message wakes still skip it.
|
|
98
|
+
if ((err as { status?: number })?.status === 404) {
|
|
99
|
+
return emptySnapshot(agentId, true);
|
|
100
|
+
}
|
|
101
|
+
runtimeLog.warn(
|
|
102
|
+
'ZiggsEffectHandler',
|
|
103
|
+
`snapshot fetch failed: ${(err as Error)?.message ?? err}`,
|
|
104
|
+
);
|
|
105
|
+
return emptySnapshot(agentId);
|
|
106
|
+
}
|
|
124
107
|
}
|
|
125
108
|
|
|
126
109
|
const OP_ARTIFACT_MAX_CHARS = 900;
|
|
@@ -178,6 +161,126 @@ async function emitToolOperationArtifact(
|
|
|
178
161
|
}
|
|
179
162
|
}
|
|
180
163
|
|
|
164
|
+
/**
|
|
165
|
+
* The single tool-execution path for BOTH cognitions (ZIG-775).
|
|
166
|
+
*
|
|
167
|
+
* Executing a tool is more than calling its handler: it emits the operation
|
|
168
|
+
* timeline artifacts (started / completed / error) and tool telemetry, and
|
|
169
|
+
* resolves the invoking user + roster. The FSM reaches this via
|
|
170
|
+
* `eff({ kind: 'tool-call' })` inside `runTurn`; the Claude Agent SDK bridge
|
|
171
|
+
* (`toolManagerToSdkMcp`) emits the SAME effect — so a hosted Claude-brain
|
|
172
|
+
* agent's tool activity is as observable as an FSM agent's (needed for
|
|
173
|
+
* hired-agent visibility, ZIG-535). A caller cannot run a tool and skip these
|
|
174
|
+
* side effects — that is the whole point of funneling both paths here.
|
|
175
|
+
*
|
|
176
|
+
* `eff` is threaded into the tool context so eff-backed tools
|
|
177
|
+
* (`ziggs_context_snapshot`, `ziggs_send_message`) resolve through this same path
|
|
178
|
+
* regardless of which cognition invoked them.
|
|
179
|
+
*/
|
|
180
|
+
export async function executeToolWithSideEffects(
|
|
181
|
+
deps: Pick<
|
|
182
|
+
ZiggsEffectDeps,
|
|
183
|
+
| 'toolManager'
|
|
184
|
+
| 'artifactsClient'
|
|
185
|
+
| 'telemetryClient'
|
|
186
|
+
| 'telemetryIngest'
|
|
187
|
+
| 'operatorKey'
|
|
188
|
+
| 'agentId'
|
|
189
|
+
| 'taskService'
|
|
190
|
+
| 'agreementService'
|
|
191
|
+
| 'contextReadClient'
|
|
192
|
+
| 'messageSender'
|
|
193
|
+
>,
|
|
194
|
+
eff: EffectHandler,
|
|
195
|
+
effect: Extract<Effect, { kind: 'tool-call' }>,
|
|
196
|
+
roster: { agents: unknown[]; users: unknown[] },
|
|
197
|
+
): Promise<ToolCallResult> {
|
|
198
|
+
const toolT0 = Date.now();
|
|
199
|
+
const args = (effect.args as Record<string, unknown>) || {};
|
|
200
|
+
const argsPreview = previewToolArgs(args);
|
|
201
|
+
await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'started', {
|
|
202
|
+
state: 'started',
|
|
203
|
+
tool: effect.name,
|
|
204
|
+
...(argsPreview ? { args: argsPreview } : {}),
|
|
205
|
+
});
|
|
206
|
+
try {
|
|
207
|
+
const lastUser = (roster.users as Array<{ userId?: string; id?: string }>).find(
|
|
208
|
+
(u) => u.userId || u.id,
|
|
209
|
+
);
|
|
210
|
+
// Accumulate token usage from any nested LLM call the tool makes (llmTool),
|
|
211
|
+
// so it can be folded into the turn's authoritative token count (ZIG-813).
|
|
212
|
+
let nestedUsage: { prompt_tokens: number; completion_tokens: number; total_tokens: number } | undefined;
|
|
213
|
+
const reportUsage = (u: LlmResponse['usage']): void => {
|
|
214
|
+
if (!u) return;
|
|
215
|
+
const p = u.prompt_tokens ?? u.promptTokens ?? 0;
|
|
216
|
+
const c = u.completion_tokens ?? u.completionTokens ?? 0;
|
|
217
|
+
const t = u.total_tokens ?? u.totalTokens ?? p + c;
|
|
218
|
+
nestedUsage = {
|
|
219
|
+
prompt_tokens: (nestedUsage?.prompt_tokens ?? 0) + p,
|
|
220
|
+
completion_tokens: (nestedUsage?.completion_tokens ?? 0) + c,
|
|
221
|
+
total_tokens: (nestedUsage?.total_tokens ?? 0) + t,
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
const result = await deps.toolManager.executeTool(effect.name, args, {
|
|
225
|
+
operatorKey: deps.operatorKey,
|
|
226
|
+
agentId: deps.agentId,
|
|
227
|
+
chatId: effect.sessionId,
|
|
228
|
+
taskService: deps.taskService,
|
|
229
|
+
agreementService: deps.agreementService,
|
|
230
|
+
contextReadClient: deps.contextReadClient,
|
|
231
|
+
sendMessage: deps.messageSender,
|
|
232
|
+
eff,
|
|
233
|
+
agents: roster.agents,
|
|
234
|
+
users: roster.users,
|
|
235
|
+
senderId: effect.senderId ?? lastUser?.userId ?? lastUser?.id,
|
|
236
|
+
senderType: effect.senderType ?? (lastUser ? 'USER' : undefined),
|
|
237
|
+
onBehalfOfAgreementId: effect.onBehalfOfAgreementId,
|
|
238
|
+
memory: effect.memory,
|
|
239
|
+
reportUsage,
|
|
240
|
+
turnScratch: {},
|
|
241
|
+
});
|
|
242
|
+
await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'completed', {
|
|
243
|
+
state: 'completed',
|
|
244
|
+
tool: effect.name,
|
|
245
|
+
result: clipForOperationArtifact(result),
|
|
246
|
+
});
|
|
247
|
+
sendTelemetry(
|
|
248
|
+
deps.telemetryClient,
|
|
249
|
+
buildToolIngestPayload({
|
|
250
|
+
sessionId: effect.sessionId,
|
|
251
|
+
tool: effect.name,
|
|
252
|
+
durationMs: Date.now() - toolT0,
|
|
253
|
+
ok: true,
|
|
254
|
+
runId: effect.runId,
|
|
255
|
+
stateId: effect.stateId,
|
|
256
|
+
}),
|
|
257
|
+
isTelemetryIngestEnabled(deps.telemetryIngest),
|
|
258
|
+
);
|
|
259
|
+
return { ok: true, result, ...(nestedUsage ? { usage: nestedUsage } : {}) } satisfies ToolCallResult;
|
|
260
|
+
} catch (error: unknown) {
|
|
261
|
+
const errMsg = (error as Error)?.message ?? String(error);
|
|
262
|
+
runtimeLog.warn('ZiggsEffectHandler', `tool-call "${effect.name}" failed: ${errMsg}`);
|
|
263
|
+
await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'error', {
|
|
264
|
+
state: 'failed',
|
|
265
|
+
tool: effect.name,
|
|
266
|
+
error: errMsg,
|
|
267
|
+
});
|
|
268
|
+
sendTelemetry(
|
|
269
|
+
deps.telemetryClient,
|
|
270
|
+
buildToolIngestPayload({
|
|
271
|
+
sessionId: effect.sessionId,
|
|
272
|
+
tool: effect.name,
|
|
273
|
+
durationMs: Date.now() - toolT0,
|
|
274
|
+
ok: false,
|
|
275
|
+
runId: effect.runId,
|
|
276
|
+
stateId: effect.stateId,
|
|
277
|
+
}),
|
|
278
|
+
isTelemetryIngestEnabled(deps.telemetryIngest),
|
|
279
|
+
);
|
|
280
|
+
return { ok: false, error: errMsg } satisfies ToolCallResult;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
181
284
|
/**
|
|
182
285
|
* Build a single shared EffectHandler. The handler is stateful — it caches
|
|
183
286
|
* the latest ContextSnapshot per sessionId so tool-call effects can resolve
|
|
@@ -209,10 +312,10 @@ export function createZiggsEffectHandler(deps: ZiggsEffectDeps): EffectHandler {
|
|
|
209
312
|
const dispatch = async (effect: Effect): Promise<unknown> => {
|
|
210
313
|
switch (effect.kind) {
|
|
211
314
|
case 'read-context': {
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
//
|
|
315
|
+
// Grant-fenced aggregate read: the backend composes the ContextSnapshot
|
|
316
|
+
// (GET /context/snapshot) through the same access + temporal fence as
|
|
317
|
+
// the per-type reads. Every producer — ClaudeSdkAgent.tick, runTurn,
|
|
318
|
+
// and the on-demand ziggs_context_snapshot tool — routes through here.
|
|
216
319
|
const snap = await buildSnapshot(deps, effect.sessionId, deps.agentId, {
|
|
217
320
|
limit: effect.opts?.maxMessages,
|
|
218
321
|
});
|
|
@@ -222,44 +325,9 @@ export function createZiggsEffectHandler(deps: ZiggsEffectDeps): EffectHandler {
|
|
|
222
325
|
return snap;
|
|
223
326
|
}
|
|
224
327
|
|
|
225
|
-
case 'list-messages': {
|
|
226
|
-
return deps.messagesClient.list(effect.chatId, {
|
|
227
|
-
after: effect.after,
|
|
228
|
-
limit: effect.limit,
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
case 'list-artifacts': {
|
|
233
|
-
return deps.artifactsClient.list(
|
|
234
|
-
{ chatId: effect.chatId, agreementId: effect.agreementId },
|
|
235
|
-
{ after: effect.after, limit: effect.limit },
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
case 'list-task-history': {
|
|
240
|
-
const url = new URL(
|
|
241
|
-
`${process.env.ZIGGS_BACKEND_URL ?? 'http://localhost:3000'}/tasks/${effect.taskId}/history`,
|
|
242
|
-
);
|
|
243
|
-
if (effect.after) url.searchParams.set('after', effect.after);
|
|
244
|
-
if (effect.limit != null) url.searchParams.set('limit', String(effect.limit));
|
|
245
|
-
const res = await fetch(url.toString(), {
|
|
246
|
-
headers: {
|
|
247
|
-
'content-type': 'application/json',
|
|
248
|
-
Authorization: `Bearer ${deps.operatorKey}`,
|
|
249
|
-
'X-Agent-Id': deps.agentId,
|
|
250
|
-
},
|
|
251
|
-
});
|
|
252
|
-
if (!res.ok) {
|
|
253
|
-
throw new Error(`list-task-history ${res.status} ${res.statusText}`);
|
|
254
|
-
}
|
|
255
|
-
return res.json();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
case 'get-scope': {
|
|
259
|
-
return deps.scopeClient.get(effect.via.kind, effect.via.id);
|
|
260
|
-
}
|
|
261
328
|
|
|
262
329
|
case 'llm-call': {
|
|
330
|
+
if (!deps.llm) throw new Error('llm-call effect requires an LLM adapter — none configured for this cognition');
|
|
263
331
|
const t0 = Date.now();
|
|
264
332
|
const r: LlmResponse = await deps.llm.chatMessages(effect.messages, effect.tools);
|
|
265
333
|
sendTelemetry(
|
|
@@ -276,94 +344,44 @@ export function createZiggsEffectHandler(deps: ZiggsEffectDeps): EffectHandler {
|
|
|
276
344
|
}
|
|
277
345
|
|
|
278
346
|
case 'stream-text': {
|
|
347
|
+
if (!deps.llm) throw new Error('stream-text effect requires an LLM adapter — none configured for this cognition');
|
|
279
348
|
const { sessionId, receiverId, messageId, messages } = effect;
|
|
280
349
|
let text = '';
|
|
350
|
+
let usage: LlmResponse['usage'];
|
|
281
351
|
if (deps.llm.chatMessagesStream) {
|
|
282
|
-
|
|
283
|
-
|
|
352
|
+
const streamed = await deps.llm.chatMessagesStream(messages as LlmMessage[], (chunk) => {
|
|
353
|
+
// Live-stream display is best-effort. The transport's send rejects
|
|
354
|
+
// empty/whitespace-only content (WebSocketClient: content.trim() === ''),
|
|
355
|
+
// and a whitespace-only chunk (e.g. a leading newline) is truthy yet
|
|
356
|
+
// still throws — which, uncaught, aborts the whole stream and loses the
|
|
357
|
+
// entire reply (e.g. a composer's final synthesis). Swallow chunk-send
|
|
358
|
+
// failures; `text` accumulates independently and the final send-message
|
|
359
|
+
// (below, in runTurn) carries the full reply regardless.
|
|
360
|
+
try {
|
|
361
|
+
if (chunk && chunk.trim()) deps.chunkSender?.(sessionId, receiverId, chunk, messageId);
|
|
362
|
+
} catch { /* ignore — streaming display only */ }
|
|
284
363
|
});
|
|
364
|
+
text = streamed.text;
|
|
365
|
+
usage = streamed.usage;
|
|
285
366
|
} else {
|
|
286
367
|
const r = await deps.llm.chatMessages(messages as LlmMessage[]);
|
|
287
368
|
text = r.content ?? '';
|
|
369
|
+
usage = r.usage;
|
|
288
370
|
if (text) deps.chunkSender?.(sessionId, receiverId, text, messageId);
|
|
289
371
|
}
|
|
290
|
-
return { text };
|
|
372
|
+
return { text, usage };
|
|
291
373
|
}
|
|
292
374
|
|
|
293
375
|
case 'tool-call': {
|
|
294
|
-
|
|
376
|
+
// Roster comes from the cached snapshot (populated by read-context). Both
|
|
377
|
+
// cognitions land here — the FSM via runTurn's executeTool, the Claude
|
|
378
|
+
// Agent SDK via the toolManagerToSdkMcp bridge — so tool side effects are
|
|
379
|
+
// emitted exactly once, in one place (ZIG-775).
|
|
295
380
|
const cached = cacheGet(effect.sessionId);
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const argsPreview = previewToolArgs(args);
|
|
300
|
-
await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'started', {
|
|
301
|
-
state: 'started',
|
|
302
|
-
tool: effect.name,
|
|
303
|
-
...(argsPreview ? { args: argsPreview } : {}),
|
|
381
|
+
return executeToolWithSideEffects(deps, handler as EffectHandler, effect, {
|
|
382
|
+
agents: (cached?.agents as unknown[]) || [],
|
|
383
|
+
users: (cached?.users as unknown[]) || [],
|
|
304
384
|
});
|
|
305
|
-
try {
|
|
306
|
-
const lastUser = (users as Array<{ userId?: string; id?: string }>).find(
|
|
307
|
-
(u) => u.userId || u.id,
|
|
308
|
-
);
|
|
309
|
-
const result = await deps.toolManager.executeTool(
|
|
310
|
-
effect.name,
|
|
311
|
-
args,
|
|
312
|
-
{
|
|
313
|
-
operatorKey: deps.operatorKey,
|
|
314
|
-
agentId: deps.agentId,
|
|
315
|
-
chatId: effect.sessionId,
|
|
316
|
-
taskService: deps.taskService,
|
|
317
|
-
agreementService: deps.agreementService,
|
|
318
|
-
sendMessage: deps.messageSender,
|
|
319
|
-
agents,
|
|
320
|
-
users,
|
|
321
|
-
senderId: effect.senderId ?? lastUser?.userId ?? lastUser?.id,
|
|
322
|
-
senderType: effect.senderType ?? (lastUser ? 'USER' : undefined),
|
|
323
|
-
memory: effect.memory,
|
|
324
|
-
turnScratch: {},
|
|
325
|
-
},
|
|
326
|
-
);
|
|
327
|
-
await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'completed', {
|
|
328
|
-
state: 'completed',
|
|
329
|
-
tool: effect.name,
|
|
330
|
-
result: clipForOperationArtifact(result),
|
|
331
|
-
});
|
|
332
|
-
sendTelemetry(
|
|
333
|
-
deps.telemetryClient,
|
|
334
|
-
buildToolIngestPayload({
|
|
335
|
-
sessionId: effect.sessionId,
|
|
336
|
-
tool: effect.name,
|
|
337
|
-
durationMs: Date.now() - toolT0,
|
|
338
|
-
ok: true,
|
|
339
|
-
runId: effect.runId,
|
|
340
|
-
stateId: effect.stateId,
|
|
341
|
-
}),
|
|
342
|
-
isTelemetryIngestEnabled(deps.telemetryIngest),
|
|
343
|
-
);
|
|
344
|
-
return { ok: true, result } satisfies ToolCallResult;
|
|
345
|
-
} catch (error: unknown) {
|
|
346
|
-
const errMsg = (error as Error)?.message ?? String(error);
|
|
347
|
-
runtimeLog.warn('ZiggsEffectHandler', `tool-call "${effect.name}" failed: ${errMsg}`);
|
|
348
|
-
await emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'error', {
|
|
349
|
-
state: 'failed',
|
|
350
|
-
tool: effect.name,
|
|
351
|
-
error: errMsg,
|
|
352
|
-
});
|
|
353
|
-
sendTelemetry(
|
|
354
|
-
deps.telemetryClient,
|
|
355
|
-
buildToolIngestPayload({
|
|
356
|
-
sessionId: effect.sessionId,
|
|
357
|
-
tool: effect.name,
|
|
358
|
-
durationMs: Date.now() - toolT0,
|
|
359
|
-
ok: false,
|
|
360
|
-
runId: effect.runId,
|
|
361
|
-
stateId: effect.stateId,
|
|
362
|
-
}),
|
|
363
|
-
isTelemetryIngestEnabled(deps.telemetryIngest),
|
|
364
|
-
);
|
|
365
|
-
return { ok: false, error: errMsg } satisfies ToolCallResult;
|
|
366
|
-
}
|
|
367
385
|
}
|
|
368
386
|
|
|
369
387
|
case 'send-message': {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
proposeAgreement, proposeBroadcast as proposeBroadcastApi, delegateAgreement, respondToAgreement, counterAgreement,
|
|
3
3
|
getAgreementStatus, listAgreements, getMyAgreements, getAgreement,
|
|
4
|
-
publishOffer as publishOfferApi, createAgreement, revokeAgreement,
|
|
4
|
+
publishOffer as publishOfferApi, createAgreement, revokeAgreement, fulfillAgreement, linkAgreementToChat,
|
|
5
5
|
getChatsForAgreement, joinAgreement, linkArtifactToAgreement, getArtifactsForAgreement,
|
|
6
6
|
linkUserToAgreement, getUsersForAgreement,
|
|
7
7
|
type Creds, type Agreement,
|
|
@@ -84,8 +84,8 @@ export class AgreementService {
|
|
|
84
84
|
return revokeAgreement(agreementId, this.creds);
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
return
|
|
87
|
+
fulfill(agreementId: string): Promise<{ ok: boolean; agreement: Agreement }> {
|
|
88
|
+
return fulfillAgreement(agreementId, this.creds);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
linkToChat(agreementId: string, chatId: string, linkType: ChatLinkType = 'mention'): Promise<unknown | null> {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createTask, getTask, getActiveTasksForAgent, getActiveTasksForChat, getSubtasks,
|
|
3
|
-
updateTaskState, cancelTask,
|
|
3
|
+
updateTaskState, updateTaskPlanStep, cancelTask,
|
|
4
4
|
reportTask, setTaskSatisfaction, countTasks,
|
|
5
5
|
type Creds, type Task, type TaskState,
|
|
6
6
|
} from '@ziggs-ai/api-client';
|
|
@@ -50,14 +50,15 @@ export class TaskService {
|
|
|
50
50
|
return getSubtasks(parentTaskId, this.creds);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
async spawnUnderAgreement(agreementId: string, taskData: { description: string; parentTaskId?: string; plan?: unknown; planReviewTiming?: string; requireMidWorkPlanAck?: boolean; assigneeId?: string }): Promise<Task> {
|
|
53
|
+
async spawnUnderAgreement(agreementId: string, taskData: { description: string; parentTaskId?: string; plan?: unknown; planReviewTiming?: string; requireMidWorkPlanAck?: boolean; assigneeId?: string; inputArtifactIds?: string[] }): Promise<Task> {
|
|
54
54
|
if (!agreementId) throw new Error('TaskService.spawnUnderAgreement: agreementId is required');
|
|
55
55
|
try {
|
|
56
56
|
const result = await createTask({ ...taskData, agreementId } as Parameters<typeof createTask>[0], this.creds);
|
|
57
57
|
runtimeLog.info('TaskService', `Task spawned under agreement ${agreementId}: ${result?.taskId || 'unknown'}`);
|
|
58
58
|
return result;
|
|
59
59
|
} catch (error) {
|
|
60
|
-
|
|
60
|
+
const e = error as { message?: string; status?: number; body?: string };
|
|
61
|
+
runtimeLog.error('TaskService', `Task spawn failed (agreement ${agreementId}, assignee ${taskData.assigneeId ?? '-'}): status=${e.status ?? '?'} ${e.message}${e.body ? ` | body=${e.body}` : ''}`);
|
|
61
62
|
throw error;
|
|
62
63
|
}
|
|
63
64
|
}
|
|
@@ -74,6 +75,17 @@ export class TaskService {
|
|
|
74
75
|
}
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
async updatePlanStep(taskId: string, stepId: string, status: string, result?: unknown): Promise<Task> {
|
|
79
|
+
try {
|
|
80
|
+
const updated = await updateTaskPlanStep(taskId, stepId, status, result, this.creds);
|
|
81
|
+
runtimeLog.info('TaskService', `Task plan step: ${taskId}/${stepId} → ${status}`);
|
|
82
|
+
return updated;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
runtimeLog.error('TaskService', `Task plan step update failed: ${(error as Error).message}`);
|
|
85
|
+
throw error;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
77
89
|
async cancel(taskId: string): Promise<Task> {
|
|
78
90
|
try {
|
|
79
91
|
const updated = await cancelTask(taskId, this.creds);
|