@ziggs-ai/agent-sdk 0.10.8 → 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/CHANGELOG.md +11 -11
- package/README.md +1 -1
- package/dist/AgentHost.d.ts +96 -16
- package/dist/AgentHost.js +205 -25
- package/dist/adapters/AnthropicAdapter.js +1 -1
- package/dist/adapters/McpAdapter.d.ts +10 -10
- package/dist/adapters/McpAdapter.js +31 -21
- package/dist/adapters/OpenAIAdapter.js +1 -1
- package/dist/agent/Agent.d.ts +2 -2
- package/dist/agent/ClaudeSdkAgent.d.ts +15 -6
- package/dist/agent/ClaudeSdkAgent.js +149 -48
- package/dist/agent/claudeCognition.d.ts +3 -3
- package/dist/agent/claudeCognition.js +7 -7
- package/dist/agent/claudeSdkTools.d.ts +3 -3
- package/dist/agent/claudeSdkTools.js +34 -15
- package/dist/agent/hostedAgentConnection.d.ts +1 -1
- package/dist/agent/hostedAgentConnection.js +1 -1
- package/dist/agent/toolManagerToSdkMcp.d.ts +1 -1
- package/dist/agent/toolManagerToSdkMcp.js +2 -2
- package/dist/context/applyEffects.js +1 -1
- package/dist/defineAgent.js +33 -4
- package/dist/formatters/HistoryFormatter.js +2 -2
- package/dist/index.d.ts +4 -2
- package/dist/index.js +8 -5
- package/dist/ingress/normalizeIncoming.d.ts +1 -1
- package/dist/ingress/normalizeIncoming.js +7 -7
- package/dist/pricing/fleetDefaults.d.ts +3 -3
- package/dist/pricing/fleetDefaults.js +3 -3
- package/dist/pricing/fleetEvalFree.d.ts +1 -1
- package/dist/pricing/fleetEvalFree.js +1 -1
- package/dist/pricing/fleetFreeTierA.gen.d.ts +1 -1
- package/dist/pricing/fleetFreeTierA.gen.js +1 -1
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +2 -2
- package/dist/pricing/fleetTierByAgentId.gen.js +9 -2
- package/dist/runtime/AgentMachine.d.ts +1 -1
- package/dist/runtime/AgentMachine.js +8 -8
- package/dist/runtime/PromptBuilder.d.ts +2 -2
- package/dist/runtime/PromptBuilder.js +3 -3
- package/dist/runtime/bootstrapCtx.d.ts +1 -1
- package/dist/runtime/bootstrapCtx.js +2 -2
- package/dist/runtime/buildOutcome.d.ts +1 -1
- package/dist/runtime/buildOutcome.js +4 -4
- package/dist/runtime/defaults.d.ts +5 -5
- package/dist/runtime/defaults.js +4 -4
- package/dist/runtime/deliveryWriteKeys.d.ts +1 -1
- package/dist/runtime/deliveryWriteKeys.js +3 -3
- package/dist/runtime/kickoff.d.ts +3 -3
- package/dist/runtime/kickoff.js +3 -3
- package/dist/runtime/runMechanical.d.ts +1 -1
- package/dist/runtime/runMechanical.js +1 -1
- package/dist/runtime/runTurn.d.ts +1 -1
- package/dist/runtime/runTurn.js +10 -10
- package/dist/runtime/taskBookkeeping.d.ts +3 -3
- package/dist/runtime/taskBookkeeping.js +3 -3
- package/dist/runtime/taskIntake.js +2 -2
- package/dist/server/ConnectionPool.js +1 -1
- package/dist/server/EventQueue.d.ts +1 -1
- package/dist/server/EventQueue.js +1 -1
- package/dist/server/SeenMessages.d.ts +2 -2
- package/dist/server/SeenMessages.js +2 -2
- package/dist/server/ZiggsEffectHandler.d.ts +2 -2
- package/dist/server/ZiggsEffectHandler.js +18 -9
- package/dist/server/agreements/AgreementService.d.ts +3 -8
- package/dist/server/agreements/AgreementService.js +4 -7
- package/dist/server/inboxLoop.d.ts +5 -5
- package/dist/server/inboxLoop.js +27 -11
- package/dist/server/polls/PollRunner.d.ts +36 -1
- package/dist/server/polls/PollRunner.js +42 -2
- package/dist/server/questMatch.d.ts +1 -1
- package/dist/server/questMatch.js +1 -1
- package/dist/server/runLauncher.js +3 -3
- package/dist/server/tasks/TaskService.d.ts +5 -5
- package/dist/server/tools/index.d.ts +1 -0
- package/dist/server/tools/index.js +7 -6
- package/dist/server/tools/tier1/protocolRunner.d.ts +1 -1
- package/dist/server/tools/tier1/protocolRunner.js +19 -19
- package/dist/server/tools/tier1/protocolTools.js +4 -4
- package/dist/server/tools/tier2/artifactTools.js +2 -2
- package/dist/server/tools/tier2/chatTools.js +2 -2
- package/dist/server/tools/tier2/connectionTools.js +3 -3
- package/dist/server/tools/tier2/contextTools.js +1 -1
- package/dist/server/tools/tier2/discoveryTools.js +2 -2
- package/dist/server/tools/tier2/grantsTools.js +1 -1
- package/dist/server/tools/tier2/linkTools.js +3 -3
- package/dist/server/tools/tier2/marketplaceTools.js +1 -1
- package/dist/server/tools/tier2/mcpConnectionTools.d.ts +4 -0
- package/dist/server/tools/tier2/mcpConnectionTools.js +136 -0
- package/dist/server/tools/tier2/paymentTools.js +1 -1
- package/dist/shared/operatorKeyClaims.d.ts +2 -2
- package/dist/shared/operatorKeyClaims.js +1 -1
- package/dist/tasks/protocolRegistry.d.ts +1 -1
- package/dist/tasks/protocolRegistry.js +1 -1
- package/dist/tools/ToolManager.d.ts +1 -1
- package/dist/tools/ToolManager.js +3 -2
- package/dist/tools/capabilityAdapter.d.ts +1 -1
- package/dist/tools/capabilityAdapter.js +3 -3
- package/dist/tools/defineTool.d.ts +7 -0
- package/dist/tools/defineTool.js +13 -1
- package/dist/tools/needs.d.ts +54 -0
- package/dist/tools/needs.js +84 -0
- package/dist/tools/recordReport.d.ts +3 -3
- package/dist/tools/recordReport.js +3 -3
- package/dist/types.d.ts +28 -18
- package/package.json +3 -3
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';
|
|
@@ -40,13 +41,13 @@ async function loadConnectionPool() {
|
|
|
40
41
|
}
|
|
41
42
|
const DEFAULT_SESSION_RESOLVER = (normalized) => normalized.chatId;
|
|
42
43
|
/**
|
|
43
|
-
*
|
|
44
|
+
* one lane key per unit of work, derived the same way as inbox wakes.
|
|
44
45
|
*
|
|
45
46
|
* Inbox deliveries key on `chatId` (DEFAULT_SESSION_RESOLVER). Bootstrap used to
|
|
46
47
|
* prefer bare `agreementId`, which opened a second lane for the same task and
|
|
47
48
|
* made the snapshot read treat the agreement id as a chat (`chatMissing: true`,
|
|
48
49
|
* empty context). Prefer the origin chat when present; otherwise the agreement's
|
|
49
|
-
* notification chat (`agrn-<agreementId>`), which is always linked after
|
|
50
|
+
* notification chat (`agrn-<agreementId>`), which is always linked after.
|
|
50
51
|
*/
|
|
51
52
|
export function laneSessionIdForTask(task) {
|
|
52
53
|
if (typeof task.originChatId === 'string' && task.originChatId) {
|
|
@@ -69,13 +70,13 @@ function createToolManager(options) {
|
|
|
69
70
|
taskTools = PROTOCOL_TOOLS; // 'all' or undefined
|
|
70
71
|
const claudeSdkTools = isClaudeCognition(options.cognition) ? CLAUDE_SDK_TOOLS : [];
|
|
71
72
|
const ownTools = Array.isArray(userTools) ? userTools : [];
|
|
72
|
-
//
|
|
73
|
+
// browse rides wherever claim rides. Posted-first engagements
|
|
73
74
|
// START at marketplace_view (find the listing), so an agent that can
|
|
74
75
|
// agreement_claim but cannot browse holds a default pathway it cannot begin.
|
|
75
76
|
// Both directions: shedding claim (taskTools array without it) sheds browse.
|
|
76
77
|
const canClaim = taskTools.some((t) => t.schema.function.name === 'agreement_claim');
|
|
77
78
|
const browseTools = canClaim ? [marketplaceViewTool] : [];
|
|
78
|
-
//
|
|
79
|
+
// chat_open is always-on so claim/provider kickoff (FSM) and the
|
|
79
80
|
// Claude always-on set can open the buyer chat after a store claim. CHAT_TOOLS
|
|
80
81
|
// is also folded into CLAUDE_SDK_TOOLS; Map registration makes duplicates a no-op.
|
|
81
82
|
const platformTools = [...taskTools, ...CHAT_TOOLS, ...browseTools, ...claudeSdkTools];
|
|
@@ -128,7 +129,7 @@ function createBackendServices(options) {
|
|
|
128
129
|
const agentId = options.agentId;
|
|
129
130
|
const artifactsClient = new ArtifactsClient(operatorKey, agentId);
|
|
130
131
|
const contextReadClient = new ContextReadClient(operatorKey, agentId);
|
|
131
|
-
//
|
|
132
|
+
// the role text is the prompt when the author split the two; the
|
|
132
133
|
// store-facing `description` is only the fallback for agents that never did.
|
|
133
134
|
const promptBuilder = new PromptBuilder({
|
|
134
135
|
description: options.systemPrompt || options.description,
|
|
@@ -153,7 +154,7 @@ function createBackendServices(options) {
|
|
|
153
154
|
* a transport (Ziggs WebSocket), and the lane router (per-session FIFO queue
|
|
154
155
|
* + in-memory session state).
|
|
155
156
|
*
|
|
156
|
-
* transport (WS) → handleMessage → agent.tick(
|
|
157
|
+
* transport (WS) → handleMessage → agent.tick(.. , eff) → ..
|
|
157
158
|
*/
|
|
158
159
|
export class AgentHost {
|
|
159
160
|
options;
|
|
@@ -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)
|
|
@@ -319,7 +328,7 @@ export class AgentHost {
|
|
|
319
328
|
agentId: this.options.agentId,
|
|
320
329
|
// No taskService/agreementService: Claude-brain tools execute through
|
|
321
330
|
// eff({ kind: 'tool-call' }) → the shared executor, which sources those
|
|
322
|
-
// services from the effect handler's deps like the FSM path
|
|
331
|
+
// services from the effect handler's deps like the FSM path.
|
|
323
332
|
maxTurns: this.options.claudeMaxTurns,
|
|
324
333
|
statelessPerWake: this.options.claudeStatelessPerWake,
|
|
325
334
|
})
|
|
@@ -331,7 +340,7 @@ export class AgentHost {
|
|
|
331
340
|
this.eventQueue = new EventQueue((event, laneKey) => this._processEvent(event, laneKey));
|
|
332
341
|
// A timer frame is not a delivery: there is no watermark waiting on it and
|
|
333
342
|
// nothing to redeliver, so its failure is logged and dropped here rather
|
|
334
|
-
// than left as an unhandled rejection (
|
|
343
|
+
// than left as an unhandled rejection (made enqueue reject).
|
|
335
344
|
this.stateTimers = new StateTimers((ev, laneKey) => {
|
|
336
345
|
void this.eventQueue.enqueue(ev, laneKey).catch((err) => {
|
|
337
346
|
runtimeLog.warn('AgentHost', `state-timer frame failed laneKey=${laneKey}: ${err?.message || err}`);
|
|
@@ -366,7 +375,7 @@ export class AgentHost {
|
|
|
366
375
|
event: { type: 'message', timestamp: Date.now(), text: '' },
|
|
367
376
|
};
|
|
368
377
|
let sessionId = this.sessionResolver(fakeNormalized, ownAgentId);
|
|
369
|
-
//
|
|
378
|
+
// store claims deliver agreement wakes with no chatId. Do not
|
|
370
379
|
// drop them; lane by agreement the same way task bootstrap does
|
|
371
380
|
// (`agrn-<agreementId>`), never a bare agreement id.
|
|
372
381
|
if (!sessionId) {
|
|
@@ -415,7 +424,7 @@ export class AgentHost {
|
|
|
415
424
|
}
|
|
416
425
|
let enrichedText = typeof text === 'string' ? text : '';
|
|
417
426
|
if (!enrichedText.trim()) {
|
|
418
|
-
//
|
|
427
|
+
// agreement rides under `service` on the inbox-pull wire; fall
|
|
419
428
|
// back to top-level for hand-built metadata.
|
|
420
429
|
const svc = metadata.service;
|
|
421
430
|
const agreement = (svc?.agreement ?? metadata.agreement);
|
|
@@ -441,7 +450,7 @@ export class AgentHost {
|
|
|
441
450
|
// routes the event onto its lane. No live-path watermark ack — there is no
|
|
442
451
|
// separate live/push channel to reconcile against anymore.
|
|
443
452
|
//
|
|
444
|
-
//
|
|
453
|
+
// a failure un-marks the messageId. The mark above is what stops
|
|
445
454
|
// the same message being processed twice; leaving it set on a failed wake
|
|
446
455
|
// would make the redelivery this rejection buys us a no-op.
|
|
447
456
|
try {
|
|
@@ -454,7 +463,7 @@ export class AgentHost {
|
|
|
454
463
|
}
|
|
455
464
|
}
|
|
456
465
|
/**
|
|
457
|
-
*
|
|
466
|
+
* part 3 — an agreement lane outlives the agreement.
|
|
458
467
|
*
|
|
459
468
|
* A lane named `agrn-<agreementId>` keeps its Claude session for the whole
|
|
460
469
|
* engagement, so a trailing event after the deal ends pays for all of it: on
|
|
@@ -486,11 +495,11 @@ export class AgentHost {
|
|
|
486
495
|
}
|
|
487
496
|
}
|
|
488
497
|
/**
|
|
489
|
-
*
|
|
498
|
+
* the trailing turn on work that is already reported.
|
|
490
499
|
*
|
|
491
500
|
* A lane session carries the whole engagement, so the LAST wake is the most
|
|
492
501
|
* expensive one: on prod, the frame that followed a completed elicitation cost
|
|
493
|
-
* **158,733 tokens** with the session's input at 366,417 before
|
|
502
|
+
* **158,733 tokens** with the session's input at 366,417 before reset
|
|
494
503
|
* it, and produced nothing — the spec had been written and the task closed one
|
|
495
504
|
* frame earlier.
|
|
496
505
|
*
|
|
@@ -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).
|
|
@@ -589,7 +704,7 @@ export class AgentHost {
|
|
|
589
704
|
runtimeLog.info('AgentHost', `wake agent=${identity.agentId} lane=${laneKey} llmCalls=${m.llmCalls} ` +
|
|
590
705
|
`tokens_in=${m.tokens.input} tokens_out=${m.tokens.output} total=${m.tokens.total}`);
|
|
591
706
|
}
|
|
592
|
-
// Single source of truth for usage/cost
|
|
707
|
+
// Single source of truth for usage/cost: emit the tick's
|
|
593
708
|
// authoritative token metrics once, for every cognition. Never let an
|
|
594
709
|
// observability failure break the tick.
|
|
595
710
|
if (this.options.metricsTap && result.metrics) {
|
|
@@ -624,7 +739,7 @@ export class AgentHost {
|
|
|
624
739
|
// again unless a real event happens to arrive.
|
|
625
740
|
this._armIfTimedParked(laneKey, session.state ?? this.agent.workflow.initial);
|
|
626
741
|
// Best-effort: also notify the user so the conversation isn't silently
|
|
627
|
-
// stuck
|
|
742
|
+
// stuck. Complements the timer re-arm above — that recovers a
|
|
628
743
|
// parked lane; this tells a waiting human.
|
|
629
744
|
const ev = event;
|
|
630
745
|
const senderType = String(ev?.senderType ?? '').toUpperCase();
|
|
@@ -632,7 +747,7 @@ export class AgentHost {
|
|
|
632
747
|
if (senderId && senderType === 'USER') {
|
|
633
748
|
this.eff({ kind: 'send-message', sessionId: session.sessionId, receiverId: senderId, text: 'Something went wrong. Please try again.' }).catch(() => { });
|
|
634
749
|
}
|
|
635
|
-
//
|
|
750
|
+
// and then say so. Everything above is recovery for the lane —
|
|
636
751
|
// it does not mean the event was handled, and the caller is about to
|
|
637
752
|
// decide whether the delivery that carried it can be acked. Swallowing
|
|
638
753
|
// here is what turned a failed wake into a lost one: two hand-off publish
|
|
@@ -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)
|
|
@@ -682,7 +862,7 @@ export class AgentHost {
|
|
|
682
862
|
onResourceEvent: (event) => {
|
|
683
863
|
return this.handleResourceEvent(event);
|
|
684
864
|
},
|
|
685
|
-
//
|
|
865
|
+
// work delivered by the inbox's task channel, not by a chat
|
|
686
866
|
// row — this is what reaches a task under a chat-less agreement or one
|
|
687
867
|
// the agent assigned to itself.
|
|
688
868
|
onTask: async (task) => {
|
|
@@ -693,7 +873,7 @@ export class AgentHost {
|
|
|
693
873
|
runtimeLog.warn(this.options.name || 'agent', `inbox task ${task.taskId} could not be woken: ${err.message}`);
|
|
694
874
|
}
|
|
695
875
|
},
|
|
696
|
-
//
|
|
876
|
+
// exact-match triage in code — miss acks with 0 tokens.
|
|
697
877
|
onQuest: async (quest) => {
|
|
698
878
|
await this.triageQuestDelivery(quest);
|
|
699
879
|
},
|
|
@@ -702,7 +882,7 @@ export class AgentHost {
|
|
|
702
882
|
return this;
|
|
703
883
|
}
|
|
704
884
|
/**
|
|
705
|
-
*
|
|
885
|
+
* plain string compare against {@link AgentHostOptions.questMatchTags}
|
|
706
886
|
* and `specialization`. No match → return (caller acks). Match → normal wake.
|
|
707
887
|
*/
|
|
708
888
|
async triageQuestDelivery(quest) {
|
|
@@ -768,7 +948,7 @@ export class AgentHost {
|
|
|
768
948
|
}, laneKey);
|
|
769
949
|
}
|
|
770
950
|
}
|
|
771
|
-
/**
|
|
951
|
+
/** replay task-assigned for an existing active task (demo recovery). */
|
|
772
952
|
async wakeTaskAssignment(taskId) {
|
|
773
953
|
const agentId = this.options.agentId;
|
|
774
954
|
if (!agentId || !this.taskService) {
|
|
@@ -94,7 +94,7 @@ export class AnthropicAdapter {
|
|
|
94
94
|
const final = await stream.finalMessage();
|
|
95
95
|
const u = final.usage;
|
|
96
96
|
// Fold cache read/creation into prompt tokens so the streamed reply is not
|
|
97
|
-
// undercounted (cache traffic was the bulk of the
|
|
97
|
+
// undercounted (cache traffic was the bulk of the drain).
|
|
98
98
|
const usage = u
|
|
99
99
|
? {
|
|
100
100
|
prompt_tokens: (u.input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0),
|
|
@@ -38,7 +38,7 @@ export type McpTransportConfig = {
|
|
|
38
38
|
agentId?: string;
|
|
39
39
|
} | {
|
|
40
40
|
/**
|
|
41
|
-
* Per-user brokered connection
|
|
41
|
+
* Per-user brokered connection: the agent holds NO
|
|
42
42
|
* connectionId/grantId. It speaks StreamableHTTP to
|
|
43
43
|
* `/connections/mcp/relay` and asserts the invoking end-user via
|
|
44
44
|
* `x-on-behalf-of-user`; the gateway resolves that user's own connection
|
|
@@ -57,13 +57,13 @@ export type McpTransportConfig = {
|
|
|
57
57
|
* The MCP server URL this agent fronts. Not used for gateway calls (the
|
|
58
58
|
* gateway derives the server from the resolved connection), but needed to
|
|
59
59
|
* open a connection request when the invoking user hasn't connected yet
|
|
60
|
-
|
|
60
|
+
*. Without it, a NO_USER_CONNECTION surfaces as an error rather
|
|
61
61
|
* than an auto-created consent prompt.
|
|
62
62
|
*/
|
|
63
63
|
serverUrl?: string;
|
|
64
64
|
};
|
|
65
65
|
/**
|
|
66
|
-
* Typed failure from the Ziggs MCP gateway
|
|
66
|
+
* Typed failure from the Ziggs MCP gateway. `code` is the stable
|
|
67
67
|
* machine-readable reason — e.g. `GRANT_REVOKED`, `GRANT_EXPIRED`,
|
|
68
68
|
* `GRANT_HOLDER_MISMATCH`, `GRANT_CONNECTION_MISMATCH` — so agent code can
|
|
69
69
|
* branch ("revoked → tell the user and stop retrying" vs "network blip →
|
|
@@ -89,7 +89,7 @@ export declare function resolveZiggsGatewayTarget(config: Extract<McpTransportCo
|
|
|
89
89
|
headers: Record<string, string>;
|
|
90
90
|
};
|
|
91
91
|
/**
|
|
92
|
-
* Resolve the per-user gateway URL + headers
|
|
92
|
+
* Resolve the per-user gateway URL + headers. No connectionId/grantId —
|
|
93
93
|
* the gateway resolves the invoking user's own connection + grant from
|
|
94
94
|
* `x-on-behalf-of-user` + the impersonated agent.
|
|
95
95
|
*/
|
|
@@ -109,9 +109,9 @@ export interface CreateMcpAgentOptions extends Omit<AgentHostOptions, 'tools' |
|
|
|
109
109
|
/**
|
|
110
110
|
* Max tools to bind to the LLM per turn. LLM providers cap the `tools` array
|
|
111
111
|
* (OpenAI: 128); an MCP server exposing more (e.g. PostHog: 146) makes every
|
|
112
|
-
* turn fail with `400
|
|
112
|
+
* turn fail with `400 .. array too long`. When the server's tool count
|
|
113
113
|
* exceeds this budget the list is truncated (and logged). Defaults to 128.
|
|
114
|
-
* See
|
|
114
|
+
* See; Phase 2 will replace truncation with query-aware selection.
|
|
115
115
|
*/
|
|
116
116
|
maxTools?: number;
|
|
117
117
|
/**
|
|
@@ -120,12 +120,12 @@ export interface CreateMcpAgentOptions extends Omit<AgentHostOptions, 'tools' |
|
|
|
120
120
|
* MCP server (e.g. PostHog's 146 tools) be narrowed to the handful this agent
|
|
121
121
|
* actually needs — cutting per-turn cost and stopping the model from thrashing
|
|
122
122
|
* across (or never binding) the tool it should call. Empty/undefined = bind
|
|
123
|
-
* all (unchanged behavior).
|
|
123
|
+
* all (unchanged behavior). Phase 2 = name-based selection.
|
|
124
124
|
*/
|
|
125
125
|
allowedTools?: string[];
|
|
126
126
|
/**
|
|
127
127
|
* Pre-discovered MCP tool schemas. REQUIRED for `type: 'ziggs-per-user'`
|
|
128
|
-
*
|
|
128
|
+
* — there is no boot connection to call `tools/list` on, so the
|
|
129
129
|
* schema is baked at provision time and passed here. Ignored for the other
|
|
130
130
|
* transports, which discover tools live at boot.
|
|
131
131
|
*/
|
|
@@ -138,7 +138,7 @@ export interface CreateMcpAgentOptions extends Omit<AgentHostOptions, 'tools' |
|
|
|
138
138
|
/**
|
|
139
139
|
* How many MCP tools may be bound to the LLM, given the provider cap and the
|
|
140
140
|
* number of extra (non-MCP) tools the agent also carries. Always >= 1 so a
|
|
141
|
-
* pathological config still binds something rather than nothing.
|
|
141
|
+
* pathological config still binds something rather than nothing.
|
|
142
142
|
*/
|
|
143
143
|
export declare function mcpToolBudget(maxTools: number, extraToolCount: number): number;
|
|
144
144
|
export interface McpAgentHandle {
|
|
@@ -158,7 +158,7 @@ export declare function buildTransport(config: McpTransportConfig, defaults?: {
|
|
|
158
158
|
* that full block rides every turn. Keep the lead — what the tool does + how to
|
|
159
159
|
* call it — and drop the embedded reference; agents fetch detail via dedicated
|
|
160
160
|
* schema/help tools at call time. Tunable; big enough to preserve real guidance.
|
|
161
|
-
|
|
161
|
+
*.
|
|
162
162
|
*/
|
|
163
163
|
export declare const MAX_TOOL_DESC_CHARS = 2000;
|
|
164
164
|
/** Truncate one description at a sentence/newline boundary near the cap. */
|