@ziggs-ai/agent-sdk 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/AgentHost.d.ts +81 -1
- package/dist/AgentHost.js +184 -4
- package/dist/adapters/McpAdapter.js +10 -12
- package/dist/agent/ClaudeSdkAgent.d.ts +9 -0
- package/dist/agent/ClaudeSdkAgent.js +127 -26
- package/dist/agent/claudeSdkTools.js +25 -6
- package/dist/defineAgent.js +31 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -0
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +2 -2
- package/dist/pricing/fleetTierByAgentId.gen.js +9 -2
- package/dist/server/ZiggsEffectHandler.js +10 -1
- package/dist/server/inboxLoop.js +8 -3
- package/dist/server/polls/PollRunner.d.ts +36 -1
- package/dist/server/polls/PollRunner.js +42 -2
- package/dist/server/tasks/TaskService.d.ts +5 -5
- package/dist/server/tools/tier2/mcpConnectionTools.js +1 -1
- package/dist/tools/ToolManager.d.ts +1 -1
- package/dist/tools/ToolManager.js +2 -1
- package/dist/tools/defineTool.d.ts +7 -0
- package/dist/tools/defineTool.js +12 -0
- package/dist/tools/needs.d.ts +54 -0
- package/dist/tools/needs.js +84 -0
- package/dist/types.d.ts +11 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -91,7 +91,7 @@ The public **`Brain`** interface is `tick(TickInput): Promise<TickOutput>`. Buil
|
|
|
91
91
|
2. Pass **`openaiKey`**, **`operatorKey`** (Ziggs operator token), **`agentId`**, etc., and **`createAgent(config)`** (returns an **`AgentHost`**).
|
|
92
92
|
3. **`connect()`** to go live; the SDK long-polls the HTTP inbox and routes deliveries into **`handleMessage`**.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Worked examples and the full build path: https://ziggsai.com/docs
|
|
95
95
|
|
|
96
96
|
Point the shared HTTP client at a non-default backend (self-hosted / local) before constructing hosts — no direct `@ziggs-ai/api-client` dependency needed:
|
|
97
97
|
|
package/dist/AgentHost.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentConfig, Ctx, Workflow } from './types.js';
|
|
2
|
-
import { Agent } from './agent/Agent.js';
|
|
2
|
+
import { Agent, type TickOutput } from './agent/Agent.js';
|
|
3
3
|
import { ClaudeSdkAgent, type CognitionKind } from './agent/ClaudeSdkAgent.js';
|
|
4
4
|
import { type BrainSelection } from './brain/index.js';
|
|
5
5
|
export type { CognitionKind };
|
|
@@ -25,6 +25,7 @@ export interface SessionStore {
|
|
|
25
25
|
import { ToolManager } from './tools/ToolManager.js';
|
|
26
26
|
import type { ToolDefinition } from './tools/defineTool.js';
|
|
27
27
|
import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
28
|
+
import { type AgentNeed } from './tools/needs.js';
|
|
28
29
|
/**
|
|
29
30
|
* The single fleet-wide default model — the one place the fallback lives.
|
|
30
31
|
* Change this (or set `LLM_MODEL`) to move the whole fleet; nothing hardcodes a
|
|
@@ -82,6 +83,12 @@ export interface AgentHostOptions {
|
|
|
82
83
|
*/
|
|
83
84
|
questMatchTags?: string[];
|
|
84
85
|
tools?: ToolDefinition[];
|
|
86
|
+
/**
|
|
87
|
+
* Access this agent cannot work without. Enforced at task start:
|
|
88
|
+
* a task arriving with no live grant behind a declared need is failed with the
|
|
89
|
+
* missing access named, before the tick spends anything.
|
|
90
|
+
*/
|
|
91
|
+
needs?: AgentNeed[];
|
|
85
92
|
taskTools?: 'all' | 'none' | string[];
|
|
86
93
|
/**
|
|
87
94
|
* Brain selection — preferred over legacy `cognition`.
|
|
@@ -142,6 +149,17 @@ export interface AgentHostOptions {
|
|
|
142
149
|
cognition: string;
|
|
143
150
|
metrics: import('./agent/Agent.js').TickOutput['metrics'];
|
|
144
151
|
}) => void;
|
|
152
|
+
/**
|
|
153
|
+
* Called with everything a wake consists of, immediately before the brain
|
|
154
|
+
* runs. Pair it with {@link AgentHost.replayWake} to re-run that exact wake
|
|
155
|
+
* later against edited agent source — the difference between iterating on a
|
|
156
|
+
* prompt in seconds and standing a whole world back up to see one turn.
|
|
157
|
+
*
|
|
158
|
+
* Emitted for every wake, including ones the brain then declines to spend an
|
|
159
|
+
* LLM call on, because "why did nothing happen?" is the question a recording
|
|
160
|
+
* most often has to answer.
|
|
161
|
+
*/
|
|
162
|
+
wakeTap?: (record: WakeRecord) => void;
|
|
145
163
|
/** Production monitoring ingest. Default on; set false to disable. */
|
|
146
164
|
telemetryIngest?: boolean;
|
|
147
165
|
/**
|
|
@@ -150,6 +168,28 @@ export interface AgentHostOptions {
|
|
|
150
168
|
*/
|
|
151
169
|
inboxCatchUp?: boolean;
|
|
152
170
|
}
|
|
171
|
+
/**
|
|
172
|
+
* One wake, as the brain received it.
|
|
173
|
+
*
|
|
174
|
+
* These four fields ARE the tick's input (`agent.tick({ event, ctx, state,
|
|
175
|
+
* identity })`) — everything else a tick touches is rebuilt from the agent's own
|
|
176
|
+
* config. So a record is replayable on its own, and a replay that behaves
|
|
177
|
+
* differently differs because the agent changed, not because the input drifted.
|
|
178
|
+
*
|
|
179
|
+
* Plain JSON on purpose: a record is meant to survive being written to a file
|
|
180
|
+
* and read back by another process.
|
|
181
|
+
*/
|
|
182
|
+
export interface WakeRecord {
|
|
183
|
+
agentId: string;
|
|
184
|
+
sessionId: string;
|
|
185
|
+
laneKey: string;
|
|
186
|
+
/** The event frame that woke the agent. */
|
|
187
|
+
event: unknown;
|
|
188
|
+
/** FSM context carried into the tick — undefined on a cold lane. */
|
|
189
|
+
ctx?: unknown;
|
|
190
|
+
/** FSM state the lane was parked in — undefined on a cold lane. */
|
|
191
|
+
state?: string;
|
|
192
|
+
}
|
|
153
193
|
/**
|
|
154
194
|
* ToolManager is a Map keyed by tool name, so the last registration silently
|
|
155
195
|
* wins. An agent's own tools register last — which means an `extraTools` entry
|
|
@@ -189,6 +229,8 @@ export declare class AgentHost {
|
|
|
189
229
|
/** The per-agent inbox long-poll loop — this agent's sole wake source. */
|
|
190
230
|
private inboxLoop;
|
|
191
231
|
private inboxLoopEnabled;
|
|
232
|
+
/** Short-lived grant-provider cache for the declared-needs gate. */
|
|
233
|
+
private grantProviderCache;
|
|
192
234
|
constructor(options?: AgentHostOptions);
|
|
193
235
|
private _validate;
|
|
194
236
|
private _setup;
|
|
@@ -245,7 +287,45 @@ export declare class AgentHost {
|
|
|
245
287
|
* still gets its turn, because that is an agent with something left to say.
|
|
246
288
|
*/
|
|
247
289
|
private isSettledTaskEcho;
|
|
290
|
+
/** Providers of this agent's live connection grants. Its own seam, so the gate below is testable without a backend. */
|
|
291
|
+
private _listGrantProviders;
|
|
292
|
+
/**
|
|
293
|
+
* Short TTL on purpose: the gate runs on every wake, and a claim produces
|
|
294
|
+
* several in seconds — but hire comes before grant, so a grant arriving
|
|
295
|
+
* mid-engagement has to take effect without a restart.
|
|
296
|
+
*/
|
|
297
|
+
private _grantProvidersCached;
|
|
298
|
+
/**
|
|
299
|
+
* Refuse work this agent has no way to do, before it costs a turn.
|
|
300
|
+
* Returns true when work was refused and the tick must not run.
|
|
301
|
+
*
|
|
302
|
+
* Enforced at wake for an agent holding assigned work, not at hire: hire comes
|
|
303
|
+
* first and the grant second, so a hire-time gate would refuse every honest
|
|
304
|
+
* one. Not limited to the task frame either — a store claim wakes the agent on
|
|
305
|
+
* the agreement first, and a task-only gate let the model take that turn and
|
|
306
|
+
* reason its own way to "I have no grant" (measured on linear-janitor: 8 calls,
|
|
307
|
+
* 114,025 tokens, for what one HTTP call answers).
|
|
308
|
+
*
|
|
309
|
+
* It fails the task rather than skipping quietly, which would leave it active
|
|
310
|
+
* with nothing working it — the same invisible stall from the buyer's side.
|
|
311
|
+
*/
|
|
312
|
+
private _refuseIfNeedsUnmet;
|
|
248
313
|
private _processEvent;
|
|
314
|
+
/**
|
|
315
|
+
* Re-run one recorded wake against THIS host's agent, and nothing else.
|
|
316
|
+
*
|
|
317
|
+
* The dev loop for a prompt: record a wake off a live run, edit the agent, and
|
|
318
|
+
* see what it does differently in seconds instead of standing a world back up
|
|
319
|
+
* for one turn. The brain, tools and effect handler are the real ones, so the
|
|
320
|
+
* turn is real — its tool calls hit the backend this host is pointed at, which
|
|
321
|
+
* is why it belongs against a disposable world and not a shared one.
|
|
322
|
+
*
|
|
323
|
+
* Deliberately NOT routed through `_processEvent`: everything that method does
|
|
324
|
+
* around the tick is lane bookkeeping — session state, liveness timers, session
|
|
325
|
+
* persistence, ack decisions, failure re-arm — and a replay has no lane to keep.
|
|
326
|
+
* Borrowing that path would make a dev tool able to disturb a live one.
|
|
327
|
+
*/
|
|
328
|
+
replayWake(record: WakeRecord): Promise<TickOutput>;
|
|
249
329
|
/** Arms the lane's liveness timer iff stateName is a parked state declaring timeoutMs. */
|
|
250
330
|
private _armIfTimedParked;
|
|
251
331
|
private _resolveOwnAgentId;
|
package/dist/AgentHost.js
CHANGED
|
@@ -16,7 +16,8 @@ import { PROTOCOL_TOOLS } from './server/tools/tier1/protocolTools.js';
|
|
|
16
16
|
import { marketplaceViewTool } from './server/tools/tier2/marketplaceTools.js';
|
|
17
17
|
import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
18
18
|
import { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
|
|
19
|
-
import { ArtifactsClient, ContextReadClient, TelemetryClient, sendChatMessage, } from '@ziggs-ai/api-client';
|
|
19
|
+
import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient, sendChatMessage, } from '@ziggs-ai/api-client';
|
|
20
|
+
import { normalizeNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
|
|
20
21
|
import { TaskService } from './server/tasks/TaskService.js';
|
|
21
22
|
import { AgreementService } from './server/agreements/AgreementService.js';
|
|
22
23
|
import { runInboxLoop } from './server/inboxLoop.js';
|
|
@@ -177,6 +178,8 @@ export class AgentHost {
|
|
|
177
178
|
/** The per-agent inbox long-poll loop — this agent's sole wake source. */
|
|
178
179
|
inboxLoop = null;
|
|
179
180
|
inboxLoopEnabled;
|
|
181
|
+
/** Short-lived grant-provider cache for the declared-needs gate. */
|
|
182
|
+
grantProviderCache = null;
|
|
180
183
|
constructor(options = {}) {
|
|
181
184
|
const resolved = applyBrainToHostOptions(options);
|
|
182
185
|
this.options = {
|
|
@@ -190,6 +193,11 @@ export class AgentHost {
|
|
|
190
193
|
systemPrompt: resolved.systemPrompt,
|
|
191
194
|
specialization: resolved.specialization ?? null,
|
|
192
195
|
tools: resolved.tools || [],
|
|
196
|
+
// Copied deliberately: this object is an allow-list, so a field left out
|
|
197
|
+
// here is silently dropped between the manifest and the runtime. That is
|
|
198
|
+
// exactly how `respondsToProposals` reached the store as null for 133
|
|
199
|
+
// agents — the declaration existed and nothing carried it.
|
|
200
|
+
needs: resolved.needs,
|
|
193
201
|
taskTools: resolved.taskTools,
|
|
194
202
|
states: resolved.states,
|
|
195
203
|
initial: resolved.initial,
|
|
@@ -200,6 +208,7 @@ export class AgentHost {
|
|
|
200
208
|
effectTap: resolved.effectTap,
|
|
201
209
|
transitionTap: resolved.transitionTap,
|
|
202
210
|
metricsTap: resolved.metricsTap,
|
|
211
|
+
wakeTap: resolved.wakeTap,
|
|
203
212
|
inboxCatchUp: resolved.inboxCatchUp,
|
|
204
213
|
brain: resolved.brain,
|
|
205
214
|
cognition: resolved.cognition ?? 'fsm',
|
|
@@ -209,9 +218,9 @@ export class AgentHost {
|
|
|
209
218
|
this.inboxLoopEnabled = options.inboxCatchUp !== false;
|
|
210
219
|
this.sessionResolver = options.sessionResolver || DEFAULT_SESSION_RESOLVER;
|
|
211
220
|
// A Claude-brain wake legitimately runs long (multi-turn query, prose
|
|
212
|
-
// deliverables)
|
|
213
|
-
// which then
|
|
214
|
-
//
|
|
221
|
+
// deliverables). At 120s the watchdog cuts real spec-writing wakes
|
|
222
|
+
// mid-flight, which then redeliver and re-run. FSM turns stay on the tight
|
|
223
|
+
// default.
|
|
215
224
|
this.tickTimeoutMs =
|
|
216
225
|
options.tickTimeoutMs ??
|
|
217
226
|
(isClaudeCognition(options.cognition)
|
|
@@ -518,6 +527,90 @@ export class AgentHost {
|
|
|
518
527
|
const reported = result.result;
|
|
519
528
|
return reported !== undefined && reported !== null && reported !== '';
|
|
520
529
|
}
|
|
530
|
+
/** Providers of this agent's live connection grants. Its own seam, so the gate below is testable without a backend. */
|
|
531
|
+
async _listGrantProviders(operatorKey, agentId) {
|
|
532
|
+
const groups = await new ConnectionsClient(operatorKey, agentId).listForHolder();
|
|
533
|
+
return groups.filter((g) => (g.grants ?? []).length > 0).map((g) => g.provider);
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Short TTL on purpose: the gate runs on every wake, and a claim produces
|
|
537
|
+
* several in seconds — but hire comes before grant, so a grant arriving
|
|
538
|
+
* mid-engagement has to take effect without a restart.
|
|
539
|
+
*/
|
|
540
|
+
async _grantProvidersCached(operatorKey, agentId) {
|
|
541
|
+
const now = Date.now();
|
|
542
|
+
if (this.grantProviderCache && now - this.grantProviderCache.at < 30_000) {
|
|
543
|
+
return this.grantProviderCache.providers;
|
|
544
|
+
}
|
|
545
|
+
const providers = await this._listGrantProviders(operatorKey, agentId);
|
|
546
|
+
this.grantProviderCache = { at: now, providers };
|
|
547
|
+
return providers;
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* Refuse work this agent has no way to do, before it costs a turn.
|
|
551
|
+
* Returns true when work was refused and the tick must not run.
|
|
552
|
+
*
|
|
553
|
+
* Enforced at wake for an agent holding assigned work, not at hire: hire comes
|
|
554
|
+
* first and the grant second, so a hire-time gate would refuse every honest
|
|
555
|
+
* one. Not limited to the task frame either — a store claim wakes the agent on
|
|
556
|
+
* the agreement first, and a task-only gate let the model take that turn and
|
|
557
|
+
* reason its own way to "I have no grant" (measured on linear-janitor: 8 calls,
|
|
558
|
+
* 114,025 tokens, for what one HTTP call answers).
|
|
559
|
+
*
|
|
560
|
+
* It fails the task rather than skipping quietly, which would leave it active
|
|
561
|
+
* with nothing working it — the same invisible stall from the buyer's side.
|
|
562
|
+
*/
|
|
563
|
+
async _refuseIfNeedsUnmet(_event, _laneKey) {
|
|
564
|
+
const needs = normalizeNeeds(this.options.needs);
|
|
565
|
+
if (!needs.length)
|
|
566
|
+
return false;
|
|
567
|
+
const operatorKey = this.options.operatorKey;
|
|
568
|
+
const agentId = this.options.agentId;
|
|
569
|
+
if (!operatorKey || !agentId || !this.taskService)
|
|
570
|
+
return false;
|
|
571
|
+
let providers;
|
|
572
|
+
try {
|
|
573
|
+
providers = await this._grantProvidersCached(operatorKey, agentId);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
// A grants read that failed is not proof the grant is missing, and failing
|
|
577
|
+
// the task on our own outage would be worse than letting the agent try.
|
|
578
|
+
runtimeLog.warn(this.options.name || 'agent', `needs check skipped: ${err?.message ?? err}`);
|
|
579
|
+
return false;
|
|
580
|
+
}
|
|
581
|
+
const unmet = unmetNeeds(needs, providers);
|
|
582
|
+
if (!unmet.length)
|
|
583
|
+
return false;
|
|
584
|
+
// Only work already on this agent's plate is refused. A wake with no assigned
|
|
585
|
+
// task is someone talking to it — an agent that cannot reach Linear can still
|
|
586
|
+
// say so in a sentence, and muting it would be a worse silence.
|
|
587
|
+
let assigned;
|
|
588
|
+
try {
|
|
589
|
+
assigned = await this.taskService.getActiveTasksForAgent(agentId);
|
|
590
|
+
}
|
|
591
|
+
catch (err) {
|
|
592
|
+
runtimeLog.warn(this.options.name || 'agent', `needs check could not read assigned work: ${err?.message ?? err}`);
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
const mine = assigned.filter((t) => t.taskId && (!t.assigneeId || t.assigneeId === agentId));
|
|
596
|
+
if (!mine.length)
|
|
597
|
+
return false;
|
|
598
|
+
const message = unmetNeedsMessage(unmet);
|
|
599
|
+
for (const task of mine) {
|
|
600
|
+
const taskId = String(task.taskId);
|
|
601
|
+
runtimeLog.warn(this.options.name || 'agent', `refusing task ${taskId} — ${message}`);
|
|
602
|
+
try {
|
|
603
|
+
await this.taskService.updateState(taskId, 'failed', {
|
|
604
|
+
summary: message,
|
|
605
|
+
status: 'failed',
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
catch (err) {
|
|
609
|
+
runtimeLog.warn(this.options.name || 'agent', `could not fail task ${taskId} after refusing it: ${err?.message ?? err}`);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
521
614
|
async _processEvent(event, laneKey) {
|
|
522
615
|
const session = this.sessions.get(laneKey);
|
|
523
616
|
if (!session?.sessionId)
|
|
@@ -530,6 +623,10 @@ export class AgentHost {
|
|
|
530
623
|
runtimeLog.info(this.options.name || 'agent', `skipping wake on ${session.sessionId} — its task already reported a result`);
|
|
531
624
|
return;
|
|
532
625
|
}
|
|
626
|
+
// Before the tick spends anything: work this agent declared it cannot do
|
|
627
|
+
// without access it does not hold.
|
|
628
|
+
if (await this._refuseIfNeedsUnmet(event, laneKey))
|
|
629
|
+
return;
|
|
533
630
|
// Stale timer frames (superseded epoch) are dropped BEFORE touching the
|
|
534
631
|
// live timer — a pure-stale frame must not reset a freshly armed countdown.
|
|
535
632
|
const frame = this.stateTimers.stripStale(event, laneKey);
|
|
@@ -557,6 +654,24 @@ export class AgentHost {
|
|
|
557
654
|
sessionId: session.sessionId,
|
|
558
655
|
laneKey,
|
|
559
656
|
};
|
|
657
|
+
// Recorded here rather than after the tick: this is the last point where the
|
|
658
|
+
// input is still exactly what the brain will see, and a wake that goes on to
|
|
659
|
+
// throw is the one most worth replaying.
|
|
660
|
+
if (this.options.wakeTap) {
|
|
661
|
+
try {
|
|
662
|
+
this.options.wakeTap({
|
|
663
|
+
agentId: identity.agentId,
|
|
664
|
+
sessionId: identity.sessionId,
|
|
665
|
+
laneKey,
|
|
666
|
+
event: frame,
|
|
667
|
+
ctx: session.ctx,
|
|
668
|
+
state: session.state,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
runtimeLog.warn('AgentHost', `wakeTap failed: ${err?.message || err}`);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
560
675
|
// The watchdog timer must be cleared once the tick settles — leaving it
|
|
561
676
|
// armed pinned one live 2-minute timer per tick (and kept short-lived
|
|
562
677
|
// processes/tests alive until it fired).
|
|
@@ -644,6 +759,71 @@ export class AgentHost {
|
|
|
644
759
|
clearTimeout(tickWatchdog);
|
|
645
760
|
}
|
|
646
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Re-run one recorded wake against THIS host's agent, and nothing else.
|
|
764
|
+
*
|
|
765
|
+
* The dev loop for a prompt: record a wake off a live run, edit the agent, and
|
|
766
|
+
* see what it does differently in seconds instead of standing a world back up
|
|
767
|
+
* for one turn. The brain, tools and effect handler are the real ones, so the
|
|
768
|
+
* turn is real — its tool calls hit the backend this host is pointed at, which
|
|
769
|
+
* is why it belongs against a disposable world and not a shared one.
|
|
770
|
+
*
|
|
771
|
+
* Deliberately NOT routed through `_processEvent`: everything that method does
|
|
772
|
+
* around the tick is lane bookkeeping — session state, liveness timers, session
|
|
773
|
+
* persistence, ack decisions, failure re-arm — and a replay has no lane to keep.
|
|
774
|
+
* Borrowing that path would make a dev tool able to disturb a live one.
|
|
775
|
+
*/
|
|
776
|
+
async replayWake(record) {
|
|
777
|
+
if (!record?.sessionId)
|
|
778
|
+
throw new Error('replayWake: record.sessionId is required');
|
|
779
|
+
const identity = {
|
|
780
|
+
agentId: record.agentId || this.options.agentId,
|
|
781
|
+
sessionId: record.sessionId,
|
|
782
|
+
laneKey: record.laneKey || record.sessionId,
|
|
783
|
+
};
|
|
784
|
+
// Same watchdog as a live wake, for the same reason: an unaborted Claude
|
|
785
|
+
// query outlives the promise that abandoned it and keeps spending.
|
|
786
|
+
const abort = new AbortController();
|
|
787
|
+
let watchdog;
|
|
788
|
+
try {
|
|
789
|
+
const result = await Promise.race([
|
|
790
|
+
this.agent.tick({
|
|
791
|
+
event: record.event,
|
|
792
|
+
ctx: record.ctx,
|
|
793
|
+
state: record.state,
|
|
794
|
+
identity,
|
|
795
|
+
eff: this.eff,
|
|
796
|
+
memory: this.memory,
|
|
797
|
+
transitionTap: this.options.transitionTap,
|
|
798
|
+
signal: abort.signal,
|
|
799
|
+
}),
|
|
800
|
+
new Promise((_, reject) => {
|
|
801
|
+
watchdog = setTimeout(() => {
|
|
802
|
+
abort.abort(new Error('tick timeout'));
|
|
803
|
+
reject(new Error('tick timeout'));
|
|
804
|
+
}, this.tickTimeoutMs);
|
|
805
|
+
}),
|
|
806
|
+
]);
|
|
807
|
+
if (this.options.metricsTap && result.metrics) {
|
|
808
|
+
try {
|
|
809
|
+
this.options.metricsTap({
|
|
810
|
+
agentId: identity.agentId,
|
|
811
|
+
sessionId: identity.sessionId,
|
|
812
|
+
model: this.options.model ?? 'unknown',
|
|
813
|
+
cognition: this.options.cognition ?? 'fsm',
|
|
814
|
+
metrics: result.metrics,
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
catch (err) {
|
|
818
|
+
runtimeLog.warn('AgentHost', `metricsTap failed: ${err?.message || err}`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return result;
|
|
822
|
+
}
|
|
823
|
+
finally {
|
|
824
|
+
clearTimeout(watchdog);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
647
827
|
/** Arms the lane's liveness timer iff stateName is a parked state declaring timeoutMs. */
|
|
648
828
|
_armIfTimedParked(laneKey, stateName) {
|
|
649
829
|
if (!stateName)
|
|
@@ -56,12 +56,11 @@ export function resolveZiggsGatewayTarget(config, defaults = {}) {
|
|
|
56
56
|
if (!agentId) {
|
|
57
57
|
throw new Error('ziggs MCP transport requires an agentId to impersonate (X-Agent-Id)');
|
|
58
58
|
}
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
// backend at all, and the agent reported the server as unreachable.
|
|
59
|
+
// Explicit overrides still win, but the LAST resort is `getBackendUrl()` —
|
|
60
|
+
// the resolution every other SDK call uses — not `localhost:3000`. A deployment
|
|
61
|
+
// that sets none of BACKEND_URL / ZIGGS_BACKEND_URL would otherwise have every
|
|
62
|
+
// gateway call silently point at localhost inside its own container: the
|
|
63
|
+
// requests never reach the backend, and the agent reports it as unreachable.
|
|
65
64
|
const baseUrl = (config.baseUrl ||
|
|
66
65
|
process.env.ZIGGS_BACKEND_URL ||
|
|
67
66
|
process.env.BACKEND_URL ||
|
|
@@ -85,12 +84,11 @@ export function resolvePerUserGatewayTarget(config, agentId, userId, onBehalfOfA
|
|
|
85
84
|
if (!operatorKey) {
|
|
86
85
|
throw new Error('ziggs-per-user MCP transport requires an operatorKey (or ZIGGS_OPERATOR_KEY)');
|
|
87
86
|
}
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// backend at all, and the agent reported the server as unreachable.
|
|
87
|
+
// Explicit overrides still win, but the LAST resort is `getBackendUrl()` —
|
|
88
|
+
// the resolution every other SDK call uses — not `localhost:3000`. A deployment
|
|
89
|
+
// that sets none of BACKEND_URL / ZIGGS_BACKEND_URL would otherwise have every
|
|
90
|
+
// gateway call silently point at localhost inside its own container: the
|
|
91
|
+
// requests never reach the backend, and the agent reports it as unreachable.
|
|
94
92
|
const baseUrl = (config.baseUrl ||
|
|
95
93
|
process.env.ZIGGS_BACKEND_URL ||
|
|
96
94
|
process.env.BACKEND_URL ||
|
|
@@ -52,6 +52,15 @@ export interface ClaudeSdkAgentOptions {
|
|
|
52
52
|
* that is all the CLI gives us — no code, no typed error — so keep it narrow
|
|
53
53
|
* enough that a different failure never claims the retry.
|
|
54
54
|
*/
|
|
55
|
+
/**
|
|
56
|
+
* "I ran out of turns", as Claude Code reports it.
|
|
57
|
+
*
|
|
58
|
+
* Turn exhaustion is not a crash: the wake did real work and stopped at a cap
|
|
59
|
+
* we set. It arrives as a thrown error result with no code and no type, so the
|
|
60
|
+
* message is all there is to match on — kept narrow so no other failure claims
|
|
61
|
+
* the handling.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isMaxTurnsError(err: unknown): boolean;
|
|
55
64
|
export declare function isMissingSessionError(err: unknown): boolean;
|
|
56
65
|
/**
|
|
57
66
|
* A Ziggs agent whose brain is the Claude Agent SDK. Same host, same `eff`
|