@ziggs-ai/agent-sdk 0.20.0 → 0.21.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/dist/Agent.d.ts +28 -7
- package/dist/Agent.js +176 -71
- package/dist/brain/ClaudeBrain.d.ts +11 -1
- package/dist/brain/ClaudeBrain.js +71 -18
- package/dist/brain/ZiggsBrain.d.ts +2 -0
- package/dist/brain/ZiggsBrain.js +1 -0
- package/dist/brain/claudeCognition.d.ts +1 -1
- package/dist/brain/claudeCognition.js +47 -7
- package/dist/brain/claudeSdkProtocol.d.ts +1 -1
- package/dist/brain/claudeSdkProtocol.js +1 -1
- package/dist/brain/claudeSdkTools.d.ts +3 -0
- package/dist/brain/claudeSdkTools.js +4 -2
- package/dist/brain/hireActivationNotice.d.ts +19 -0
- package/dist/brain/hireActivationNotice.js +79 -0
- package/dist/brain/standingOfferNotice.d.ts +15 -0
- package/dist/brain/standingOfferNotice.js +89 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ingress/normalizeIncoming.d.ts +3 -0
- package/dist/ingress/normalizeIncoming.js +8 -0
- package/dist/mcp/createMcpAgent.d.ts +1 -1
- package/dist/mcp/createMcpAgent.js +1 -1
- package/dist/memory/MemoryStore.d.ts +18 -4
- package/dist/memory/MemoryStore.js +52 -12
- package/dist/platform/ConnectionPool.d.ts +9 -4
- package/dist/platform/ConnectionPool.js +16 -6
- package/dist/platform/StateTimers.d.ts +12 -6
- package/dist/platform/StateTimers.js +26 -6
- package/dist/platform/ZiggsEffectHandler.js +5 -1
- package/dist/platform/inboxLoop.d.ts +5 -7
- package/dist/platform/inboxLoop.js +36 -80
- package/dist/platform/polls/declaredPollRunner.d.ts +7 -1
- package/dist/platform/polls/declaredPollRunner.js +7 -9
- package/dist/platform/tools/index.d.ts +1 -0
- package/dist/platform/tools/index.js +1 -0
- package/dist/platform/tools/tier2/contextTools.d.ts +2 -0
- package/dist/platform/tools/tier2/contextTools.js +3 -1
- package/dist/platform/tools/tier2/planTools.d.ts +20 -0
- package/dist/platform/tools/tier2/planTools.js +64 -0
- package/dist/pricing/fleetDefaults.js +4 -9
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
- package/dist/pricing/fleetTierByAgentId.gen.js +2 -752
- package/dist/runtime/AgentMachine.d.ts +3 -0
- package/dist/runtime/AgentMachine.js +6 -1
- package/dist/runtime/runTurn.d.ts +1 -0
- package/dist/runtime/runTurn.js +19 -15
- package/dist/runtime/wakeEffects.d.ts +35 -0
- package/dist/runtime/wakeEffects.js +121 -0
- package/package.json +2 -2
- package/dist/pricing/fleetFreeTierA.gen.d.ts +0 -6
- package/dist/pricing/fleetFreeTierA.gen.js +0 -11
package/dist/Agent.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PollBinding, PollKind } from './platform/polls/PollKind.js';
|
|
2
|
+
export { workScopeFromWake } from './runtime/wakeEffects.js';
|
|
2
3
|
import type { AgentConfig, Ctx, Workflow } from './types.js';
|
|
3
4
|
import { ZiggsBrain, type TickOutput } from './brain/ZiggsBrain.js';
|
|
4
5
|
import { ClaudeBrain, type CognitionKind } from './brain/ClaudeBrain.js';
|
|
@@ -56,11 +57,6 @@ export declare function laneSessionIdForTask(task: {
|
|
|
56
57
|
originChatId?: string | null;
|
|
57
58
|
agreementId?: string | null;
|
|
58
59
|
}): string;
|
|
59
|
-
/** Pull the cost-record keys off a wake event or session id. */
|
|
60
|
-
export declare function workScopeFromWake(event: unknown, sessionId?: string): {
|
|
61
|
-
agreementId?: string;
|
|
62
|
-
taskId?: string;
|
|
63
|
-
};
|
|
64
60
|
interface LlmAdapter {
|
|
65
61
|
chatMessages(messages: unknown[], tools?: unknown[], options?: Record<string, unknown>): Promise<unknown>;
|
|
66
62
|
}
|
|
@@ -111,6 +107,12 @@ export interface AgentOptions {
|
|
|
111
107
|
* Internal mapping: `fsm` → Ziggs brain, `claude-sdk` → Claude brain.
|
|
112
108
|
*/
|
|
113
109
|
cognition?: CognitionKind;
|
|
110
|
+
/**
|
|
111
|
+
* Spend a model turn on an activation notice for a hire an agent formed.
|
|
112
|
+
* Default off — the host records a no-op and waits for the assigned task.
|
|
113
|
+
* A hire a person formed (store claim) still wakes regardless of this flag.
|
|
114
|
+
*/
|
|
115
|
+
actsOnHireActivation?: boolean;
|
|
114
116
|
/** Max agentic turns per Claude SDK wake. Default 30. */
|
|
115
117
|
claudeMaxTurns?: number;
|
|
116
118
|
/**
|
|
@@ -256,6 +258,9 @@ export declare class Agent {
|
|
|
256
258
|
private askedForAccessLanes;
|
|
257
259
|
/** Tasks left active by the asking turn, failed right after it (per lane). */
|
|
258
260
|
private unmetTasksPendingAsk;
|
|
261
|
+
private readonly wakeEffects;
|
|
262
|
+
private readonly outboundMessages;
|
|
263
|
+
private readonly confirmedPlanVersions;
|
|
259
264
|
private grantProviderCache;
|
|
260
265
|
constructor(options?: AgentOptions);
|
|
261
266
|
private _validate;
|
|
@@ -300,7 +305,21 @@ export declare class Agent {
|
|
|
300
305
|
* wake keys on the live room / hire.
|
|
301
306
|
*/
|
|
302
307
|
private dropStaleCancelledAgreementSession;
|
|
308
|
+
private formedByForHireActivation;
|
|
303
309
|
private laneAgreementIsTerminal;
|
|
310
|
+
/**
|
|
311
|
+
* Whether the host answers this wake without spending a model turn.
|
|
312
|
+
*
|
|
313
|
+
* One decision, consulted by the live path and by {@link Agent.replayWake},
|
|
314
|
+
* because a recording is only worth keeping if a replay can tell you what the
|
|
315
|
+
* host would do now. It used to run only on the live path, so an edit to any
|
|
316
|
+
* of these rules was invisible to a replay — the tool that exists to answer
|
|
317
|
+
* "what changed?" could not see the change.
|
|
318
|
+
*
|
|
319
|
+
* Every rule here reads a row or the frame itself. None of them reads a lane,
|
|
320
|
+
* an agent kind, a name or an id shape.
|
|
321
|
+
*/
|
|
322
|
+
private wakeSuppression;
|
|
304
323
|
/**
|
|
305
324
|
* the trailing turn on work that is already reported.
|
|
306
325
|
*
|
|
@@ -384,8 +403,10 @@ export declare class Agent {
|
|
|
384
403
|
*/
|
|
385
404
|
private triageRequestDelivery;
|
|
386
405
|
/** replay task-assigned for an existing active task (demo recovery). */
|
|
387
|
-
wakeTaskAssignment(taskId: string
|
|
388
|
-
|
|
406
|
+
wakeTaskAssignment(taskId: string, options?: {
|
|
407
|
+
fromInbox?: boolean;
|
|
408
|
+
}): Promise<void>;
|
|
409
|
+
disconnect(): Promise<void>;
|
|
389
410
|
/**
|
|
390
411
|
* Waits for in-flight ticks to finish. Call before process.exit() on SIGTERM.
|
|
391
412
|
* A timeout is returned, not thrown — the caller names it (lab verdict, fleet
|
package/dist/Agent.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { agreementLaneId, agreementIdFromLane } from '@ziggs-ai/api-client';
|
|
2
2
|
import { createDeclaredPollRunner } from './platform/polls/declaredPollRunner.js';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
+
import { WakeEffects } from './runtime/wakeEffects.js';
|
|
6
|
+
export { workScopeFromWake } from './runtime/wakeEffects.js';
|
|
4
7
|
import { ZiggsBrain } from './brain/ZiggsBrain.js';
|
|
5
8
|
import { ClaudeBrain } from './brain/ClaudeBrain.js';
|
|
6
9
|
import { applyBrainToAgentOptions, isClaudeCognition, } from './brain/index.js';
|
|
@@ -14,7 +17,7 @@ import { WAKE_TOOLS } from './platform/tools/tier1/wakeTools.js';
|
|
|
14
17
|
import { createZiggsEffectHandler } from './platform/ZiggsEffectHandler.js';
|
|
15
18
|
import { normalizeIncomingEvent } from './ingress/normalizeIncoming.js';
|
|
16
19
|
import { PromptBuilder } from './runtime/PromptBuilder.js';
|
|
17
|
-
import { InMemoryStore, memorySectionForWake, partitionMemoryStore, } from './memory/MemoryStore.js';
|
|
20
|
+
import { InMemoryStore, hireIdsFromEvent, memorySectionForWake, partitionMemoryStore, uniqueHireIds, } from './memory/MemoryStore.js';
|
|
18
21
|
import { ToolManager } from './tools/ToolManager.js';
|
|
19
22
|
import { toolName } from './tools/defineTool.js';
|
|
20
23
|
import { PROTOCOL_TOOLS } from './platform/tools/tier1/protocolTools.js';
|
|
@@ -31,6 +34,7 @@ import { SeenMessages } from './platform/SeenMessages.js';
|
|
|
31
34
|
import { runtimeLog } from './shared/runtimeLog.js';
|
|
32
35
|
import { isTelemetryIngestEnabled, sendTelemetry, } from './platform/telemetryIngest.js';
|
|
33
36
|
import { buildWakeOutcomePayload, classifyWakeOutcome, } from './runtime/wakeOutcome.js';
|
|
37
|
+
import { agreementIdFromWake, formedByFromWake, isHireActivationNotice, shouldSkipAgentFormedHireActivation, } from './brain/hireActivationNotice.js';
|
|
34
38
|
/**
|
|
35
39
|
* The single fleet-wide default model — the one place the fallback lives.
|
|
36
40
|
* Change this (or set `LLM_MODEL`) to move the whole fleet; nothing hardcodes a
|
|
@@ -65,26 +69,6 @@ export function laneSessionIdForTask(task) {
|
|
|
65
69
|
}
|
|
66
70
|
return 'default';
|
|
67
71
|
}
|
|
68
|
-
/** Pull the cost-record keys off a wake event or session id. */
|
|
69
|
-
export function workScopeFromWake(event, sessionId) {
|
|
70
|
-
const rec = event && typeof event === 'object' ? event : {};
|
|
71
|
-
const resource = rec.resource && typeof rec.resource === 'object'
|
|
72
|
-
? rec.resource
|
|
73
|
-
: {};
|
|
74
|
-
const agreementId = (typeof rec.agreementId === 'string' && rec.agreementId) ||
|
|
75
|
-
(typeof resource.agreementId === 'string' && resource.agreementId) ||
|
|
76
|
-
agreementIdFromLane(rec.chatId) ||
|
|
77
|
-
agreementIdFromLane(resource.chatId) ||
|
|
78
|
-
agreementIdFromLane(sessionId) ||
|
|
79
|
-
undefined;
|
|
80
|
-
const taskId = (typeof rec.taskId === 'string' && rec.taskId) ||
|
|
81
|
-
(typeof resource.taskId === 'string' && resource.taskId) ||
|
|
82
|
-
undefined;
|
|
83
|
-
return {
|
|
84
|
-
...(agreementId ? { agreementId } : {}),
|
|
85
|
-
...(taskId ? { taskId } : {}),
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
72
|
function createToolManager(options) {
|
|
89
73
|
const toolManager = new ToolManager();
|
|
90
74
|
const userTools = options.tools || options.services?.tools || [];
|
|
@@ -220,6 +204,9 @@ export class Agent {
|
|
|
220
204
|
askedForAccessLanes = new Set();
|
|
221
205
|
/** Tasks left active by the asking turn, failed right after it (per lane). */
|
|
222
206
|
unmetTasksPendingAsk = new Map();
|
|
207
|
+
wakeEffects = new AsyncLocalStorage();
|
|
208
|
+
outboundMessages = new SeenMessages();
|
|
209
|
+
confirmedPlanVersions = new SeenMessages();
|
|
223
210
|
grantProviderCache = null;
|
|
224
211
|
constructor(options = {}) {
|
|
225
212
|
const resolved = applyBrainToAgentOptions(options);
|
|
@@ -258,6 +245,7 @@ export class Agent {
|
|
|
258
245
|
polls: resolved.polls,
|
|
259
246
|
pollKinds: resolved.pollKinds,
|
|
260
247
|
cognition: resolved.cognition ?? 'fsm',
|
|
248
|
+
actsOnHireActivation: resolved.actsOnHireActivation === true,
|
|
261
249
|
claudeMaxTurns: resolved.claudeMaxTurns,
|
|
262
250
|
claudeStatelessPerWake: resolved.claudeStatelessPerWake,
|
|
263
251
|
};
|
|
@@ -313,18 +301,25 @@ export class Agent {
|
|
|
313
301
|
operatorKey: this.options.operatorKey,
|
|
314
302
|
agentId: this.options.agentId,
|
|
315
303
|
};
|
|
316
|
-
const messageSender = (message, receiverId, sessionId, messageId) =>
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
304
|
+
const messageSender = async (message, receiverId, sessionId, messageId) => {
|
|
305
|
+
const sentId = messageId ?? `sdk_${randomUUID()}`;
|
|
306
|
+
const result = await sendChatMessage({
|
|
307
|
+
chatId: sessionId,
|
|
308
|
+
// Omitted on purpose when the caller has no valid address for THIS
|
|
309
|
+
// room: the backend infers the recipient from the room's own
|
|
310
|
+
// participants.
|
|
311
|
+
receiverId,
|
|
312
|
+
text: message,
|
|
313
|
+
// messageId is the backend's (chatId, messageId) dedup key. Callers
|
|
314
|
+
// pass a wake-derived id for replay-stability; fall back to a fresh id
|
|
315
|
+
// only when absent (matches the old WS path letting the server assign).
|
|
316
|
+
messageId: sentId,
|
|
317
|
+
}, creds);
|
|
318
|
+
if (!result.success)
|
|
319
|
+
throw new Error('sendChatMessage did not confirm delivery');
|
|
320
|
+
this.outboundMessages.mark(JSON.stringify([result.chatId, result.messageId]));
|
|
321
|
+
return result;
|
|
322
|
+
};
|
|
328
323
|
this.taskService = services.taskService;
|
|
329
324
|
this.agreementService = services.agreementService;
|
|
330
325
|
this.telemetryClient = services.telemetryClient;
|
|
@@ -366,12 +361,18 @@ export class Agent {
|
|
|
366
361
|
agentId: this.options.agentId,
|
|
367
362
|
telemetryClient: services.telemetryClient,
|
|
368
363
|
telemetryIngest: this.options.telemetryIngest,
|
|
369
|
-
effectTap:
|
|
364
|
+
effectTap: (effect, response) => {
|
|
365
|
+
const marks = this.wakeEffects.getStore();
|
|
366
|
+
marks?.observe(effect, response);
|
|
367
|
+
for (const key of marks?.sentMessageKeys ?? [])
|
|
368
|
+
this.outboundMessages.mark(key);
|
|
369
|
+
this.options.effectTap?.(effect, response);
|
|
370
|
+
},
|
|
370
371
|
// `wake_at` reaches the lane's timer through here. The handler
|
|
371
372
|
// is built once and has no laneKey, so it derives one the same way the
|
|
372
373
|
// event queue does (see laneKeyFor).
|
|
373
374
|
wakeScheduler: {
|
|
374
|
-
armWakeAt: (sessionId, at, reason) => this.stateTimers.armWakeAt(laneKeyFor(this.options.agentId, sessionId), at, reason),
|
|
375
|
+
armWakeAt: (sessionId, at, reason) => this.stateTimers.armWakeAt(laneKeyFor(this.options.agentId, sessionId), at, reason, this.wakeEffects.getStore()?.scope),
|
|
375
376
|
},
|
|
376
377
|
});
|
|
377
378
|
this.brain = isClaudeCognition(this.options.cognition)
|
|
@@ -390,6 +391,7 @@ export class Agent {
|
|
|
390
391
|
// services from the effect handler's deps like the FSM path.
|
|
391
392
|
maxTurns: this.options.claudeMaxTurns,
|
|
392
393
|
statelessPerWake: this.options.claudeStatelessPerWake,
|
|
394
|
+
actsOnHireActivation: this.options.actsOnHireActivation === true,
|
|
393
395
|
})
|
|
394
396
|
: new ZiggsBrain({
|
|
395
397
|
workflow,
|
|
@@ -513,7 +515,7 @@ export class Agent {
|
|
|
513
515
|
// the same message being processed twice; leaving it set on a failed wake
|
|
514
516
|
// would make the redelivery this rejection buys us a no-op.
|
|
515
517
|
try {
|
|
516
|
-
await this.eventQueue.enqueue(normalized.event, laneKey);
|
|
518
|
+
await this.eventQueue.enqueue({ ...normalized.event, chatId: normalized.chatId }, laneKey);
|
|
517
519
|
}
|
|
518
520
|
catch (err) {
|
|
519
521
|
if (messageId)
|
|
@@ -551,12 +553,28 @@ export class Agent {
|
|
|
551
553
|
llmCalls,
|
|
552
554
|
}), isTelemetryIngestEnabled(this.options.telemetryIngest));
|
|
553
555
|
}
|
|
554
|
-
memoryForWake(session) {
|
|
555
|
-
|
|
556
|
+
memoryForWake(session, event) {
|
|
557
|
+
const fromEvent = hireIdsFromEvent(event);
|
|
558
|
+
const wakeAgreementIds = uniqueHireIds([
|
|
559
|
+
...fromEvent,
|
|
560
|
+
session.actingHireId,
|
|
561
|
+
]);
|
|
562
|
+
const section = memorySectionForWake({
|
|
556
563
|
sessionId: session.sessionId,
|
|
557
564
|
activeAgreementId: session.ctx
|
|
558
565
|
?.activeAgreementId,
|
|
559
|
-
|
|
566
|
+
wakeAgreementIds,
|
|
567
|
+
});
|
|
568
|
+
const known = uniqueHireIds([
|
|
569
|
+
session.ctx
|
|
570
|
+
?.activeAgreementId,
|
|
571
|
+
...wakeAgreementIds,
|
|
572
|
+
]);
|
|
573
|
+
if (known.length === 1)
|
|
574
|
+
session.actingHireId = known[0];
|
|
575
|
+
else if (known.length > 1)
|
|
576
|
+
session.actingHireId = null;
|
|
577
|
+
return partitionMemoryStore(this.memory, section);
|
|
560
578
|
}
|
|
561
579
|
/**
|
|
562
580
|
* A chat-lane session that still carries a cancelled hire from a previous
|
|
@@ -585,6 +603,24 @@ export class Agent {
|
|
|
585
603
|
// Fail open: a lookup error must not drop a live hire's session.
|
|
586
604
|
}
|
|
587
605
|
}
|
|
606
|
+
async formedByForHireActivation(event) {
|
|
607
|
+
const fromWake = formedByFromWake(event);
|
|
608
|
+
if (fromWake)
|
|
609
|
+
return fromWake;
|
|
610
|
+
const agreementId = agreementIdFromWake(event);
|
|
611
|
+
if (!agreementId || !this.agreementService)
|
|
612
|
+
return undefined;
|
|
613
|
+
try {
|
|
614
|
+
const agreement = await this.agreementService.get(agreementId);
|
|
615
|
+
const formedBy = agreement?.formedBy;
|
|
616
|
+
if (formedBy === 'agent' || formedBy === 'principal')
|
|
617
|
+
return formedBy;
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// Fail open: a lookup error must not drop a person's store-claim wake.
|
|
621
|
+
}
|
|
622
|
+
return undefined;
|
|
623
|
+
}
|
|
588
624
|
async laneAgreementIsTerminal(sessionId) {
|
|
589
625
|
const agreementId = agreementIdFromLane(sessionId);
|
|
590
626
|
if (!agreementId || !this.agreementService)
|
|
@@ -598,6 +634,43 @@ export class Agent {
|
|
|
598
634
|
return false;
|
|
599
635
|
}
|
|
600
636
|
}
|
|
637
|
+
/**
|
|
638
|
+
* Whether the host answers this wake without spending a model turn.
|
|
639
|
+
*
|
|
640
|
+
* One decision, consulted by the live path and by {@link Agent.replayWake},
|
|
641
|
+
* because a recording is only worth keeping if a replay can tell you what the
|
|
642
|
+
* host would do now. It used to run only on the live path, so an edit to any
|
|
643
|
+
* of these rules was invisible to a replay — the tool that exists to answer
|
|
644
|
+
* "what changed?" could not see the change.
|
|
645
|
+
*
|
|
646
|
+
* Every rule here reads a row or the frame itself. None of them reads a lane,
|
|
647
|
+
* an agent kind, a name or an id shape.
|
|
648
|
+
*/
|
|
649
|
+
async wakeSuppression(event, sessionId) {
|
|
650
|
+
if (await this.laneAgreementIsTerminal(sessionId)) {
|
|
651
|
+
return { reason: 'agreement_terminal', because: 'its agreement is terminal' };
|
|
652
|
+
}
|
|
653
|
+
if (this.isSettledTaskEcho(event)) {
|
|
654
|
+
return {
|
|
655
|
+
reason: 'task_already_settled',
|
|
656
|
+
because: 'its task already reported a result',
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
if (isHireActivationNotice(event)) {
|
|
660
|
+
const formedBy = await this.formedByForHireActivation(event);
|
|
661
|
+
if (shouldSkipAgentFormedHireActivation({
|
|
662
|
+
event,
|
|
663
|
+
formedBy,
|
|
664
|
+
actsOnHireActivation: this.options.actsOnHireActivation === true,
|
|
665
|
+
})) {
|
|
666
|
+
return {
|
|
667
|
+
reason: 'agent_formed_hire_activation',
|
|
668
|
+
because: 'agent-formed hire activation',
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
601
674
|
/**
|
|
602
675
|
* the trailing turn on work that is already reported.
|
|
603
676
|
*
|
|
@@ -780,14 +853,30 @@ export class Agent {
|
|
|
780
853
|
const session = this.sessions.get(laneKey);
|
|
781
854
|
if (!session?.sessionId)
|
|
782
855
|
return;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
856
|
+
// Presentation may call our sender an rpb-* room binding or the person we
|
|
857
|
+
// stand in for. Only an exact (chat, messageId) we successfully sent proves
|
|
858
|
+
// an echo; another message from the same binding still gets a full wake.
|
|
859
|
+
const incoming = event;
|
|
860
|
+
const events = incoming?.type === 'batch' && Array.isArray(incoming.events) ? incoming.events : [event];
|
|
861
|
+
const remaining = events.filter((item) => {
|
|
862
|
+
const e = item;
|
|
863
|
+
const task = e?.type === 'task_result' ? e.result : undefined;
|
|
864
|
+
if (this.stateTimers.peek(laneKey)?.kind === 'wake-at'
|
|
865
|
+
&& e?.source === 'active-task-snapshot' && (task?.state === 'active' || task?.state === 'in-progress')
|
|
866
|
+
&& task?.assigneeId === this.options.agentId && task.taskId && task.updatedAt && this.confirmedPlanVersions.has(JSON.stringify([laneKey, JSON.stringify([task.taskId, task.updatedAt])])))
|
|
867
|
+
return false;
|
|
868
|
+
return e?.type !== 'message' || !e.messageId || !this.outboundMessages.has(JSON.stringify([e.chatId ?? session.sessionId, e.messageId]));
|
|
869
|
+
});
|
|
870
|
+
if (remaining.length === 0) {
|
|
871
|
+
this.recordWakeOutcome(session, { kind: 'no-op', reason: events.some(e => e?.type === 'task_result') ? 'own_plan_snapshot' : 'own_message_echo' });
|
|
786
872
|
return;
|
|
787
873
|
}
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
874
|
+
if (remaining.length !== events.length)
|
|
875
|
+
event = remaining.length === 1 ? remaining[0] : { type: 'batch', events: remaining };
|
|
876
|
+
const suppressed = await this.wakeSuppression(event, session.sessionId);
|
|
877
|
+
if (suppressed) {
|
|
878
|
+
runtimeLog.info(this.options.name || 'agent', `skipping wake on ${session.sessionId} — ${suppressed.because}`);
|
|
879
|
+
this.recordWakeOutcome(session, { kind: 'no-op', reason: suppressed.reason });
|
|
791
880
|
return;
|
|
792
881
|
}
|
|
793
882
|
// Before the tick spends anything: work this agent declared it cannot do
|
|
@@ -799,9 +888,10 @@ export class Agent {
|
|
|
799
888
|
const frame = this.stateTimers.stripStale(event, laneKey);
|
|
800
889
|
if (frame === null)
|
|
801
890
|
return;
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
|
|
891
|
+
// Traffic resets inactivity timers, but must not cancel an appointment
|
|
892
|
+
// the agent explicitly requested. Only its own fresh timer frame consumes
|
|
893
|
+
// that wake; a notice-only tick cannot ask the model to restore it.
|
|
894
|
+
this.stateTimers.consumeForEvent(frame, laneKey);
|
|
805
895
|
// Cold-start recovery: restore FSM state from sessionStore when the
|
|
806
896
|
// in-memory lane has no prior context (e.g. after a container restart).
|
|
807
897
|
if (this.sessionStore && session.ctx === undefined && session.state === undefined) {
|
|
@@ -822,7 +912,7 @@ export class Agent {
|
|
|
822
912
|
sessionId: session.sessionId,
|
|
823
913
|
laneKey,
|
|
824
914
|
};
|
|
825
|
-
const memory = this.memoryForWake(session);
|
|
915
|
+
const memory = this.memoryForWake(session, frame);
|
|
826
916
|
// Recorded here rather than after the tick: this is the last point where the
|
|
827
917
|
// input is still exactly what the brain will see, and a wake that goes on to
|
|
828
918
|
// throw is the one most worth replaying.
|
|
@@ -850,20 +940,17 @@ export class Agent {
|
|
|
850
940
|
// a zombie — burning tokens and racing the redelivered retry of the same
|
|
851
941
|
// event (duplicate sends, runaway per-wake input).
|
|
852
942
|
const tickAbort = new AbortController();
|
|
853
|
-
const marks =
|
|
943
|
+
const marks = WakeEffects.forWake(frame, session.sessionId);
|
|
854
944
|
const eff = async (effect) => {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
marks.tasks += 1;
|
|
861
|
-
}
|
|
862
|
-
return this.eff(effect);
|
|
945
|
+
const response = await this.eff(effect);
|
|
946
|
+
marks.observe(effect, response);
|
|
947
|
+
for (const key of marks.sentMessageKeys)
|
|
948
|
+
this.outboundMessages.mark(key);
|
|
949
|
+
return response;
|
|
863
950
|
};
|
|
864
951
|
try {
|
|
865
952
|
const result = await Promise.race([
|
|
866
|
-
this.brain.tick({ event: frame, ctx: session.ctx, state: session.state, identity, eff, memory, transitionTap: this.options.transitionTap, signal: tickAbort.signal }),
|
|
953
|
+
this.wakeEffects.run(marks, () => this.brain.tick({ event: frame, ctx: session.ctx, state: session.state, identity, eff, memory, transitionTap: this.options.transitionTap, signal: tickAbort.signal })),
|
|
867
954
|
new Promise((_, reject) => {
|
|
868
955
|
tickWatchdog = setTimeout(() => {
|
|
869
956
|
tickAbort.abort(new Error('tick timeout'));
|
|
@@ -875,11 +962,10 @@ export class Agent {
|
|
|
875
962
|
session.state = result.state;
|
|
876
963
|
this.sessions.set(laneKey, session);
|
|
877
964
|
this.recordWakeOutcome(session, classifyWakeOutcome({
|
|
878
|
-
|
|
879
|
-
?.lastOutcome?.kind,
|
|
965
|
+
skippedReason: result.skippedReason,
|
|
880
966
|
llmCalls: result.metrics?.llmCalls ?? 0,
|
|
881
|
-
sentMessage: marks.
|
|
882
|
-
taskWrite: marks.
|
|
967
|
+
sentMessage: marks.sentMessage,
|
|
968
|
+
taskWrite: marks.taskWrite,
|
|
883
969
|
}), result.metrics?.llmCalls ?? 0);
|
|
884
970
|
// One line per wake that actually spent LLM budget — the handle for
|
|
885
971
|
// tracking a hosted agent live (grep `agent=<id>`). Parked no-op ticks
|
|
@@ -902,7 +988,7 @@ export class Agent {
|
|
|
902
988
|
model: this.options.model ?? 'unknown',
|
|
903
989
|
cognition: this.options.cognition ?? 'fsm',
|
|
904
990
|
metrics: result.metrics,
|
|
905
|
-
...
|
|
991
|
+
...marks.scope,
|
|
906
992
|
});
|
|
907
993
|
}
|
|
908
994
|
catch (err) {
|
|
@@ -934,6 +1020,14 @@ export class Agent {
|
|
|
934
1020
|
this.unmetTasksPendingAsk.delete(laneKey);
|
|
935
1021
|
this.askedForAccessLanes.delete(laneKey);
|
|
936
1022
|
}
|
|
1023
|
+
// The poll can return the row just written by this wake. Only suppress
|
|
1024
|
+
// that exact version while this lane has an explicit future wake: a
|
|
1025
|
+
// successful but unfinished turn still needs the task poll to recover
|
|
1026
|
+
// when it did not schedule its own continuation.
|
|
1027
|
+
if (this.stateTimers.peek(laneKey)?.kind === 'wake-at') {
|
|
1028
|
+
for (const version of marks.planVersions)
|
|
1029
|
+
this.confirmedPlanVersions.mark(JSON.stringify([laneKey, version]));
|
|
1030
|
+
}
|
|
937
1031
|
}
|
|
938
1032
|
catch (err) {
|
|
939
1033
|
this.recordWakeOutcome(session, {
|
|
@@ -992,17 +1086,26 @@ export class Agent {
|
|
|
992
1086
|
sessionId: record.sessionId,
|
|
993
1087
|
laneKey: record.laneKey || record.sessionId,
|
|
994
1088
|
};
|
|
1089
|
+
// The host's own answer comes first, exactly as it does live. A replay that
|
|
1090
|
+
// skipped this would report a model turn the host would never have spent,
|
|
1091
|
+
// which is the opposite of what a recording is for.
|
|
1092
|
+
const suppressed = await this.wakeSuppression(record.event, record.sessionId);
|
|
1093
|
+
if (suppressed) {
|
|
1094
|
+
runtimeLog.info(this.options.name || 'agent', `replay: no wake on ${record.sessionId} — ${suppressed.because}`);
|
|
1095
|
+
return { state: record.state };
|
|
1096
|
+
}
|
|
995
1097
|
const memory = this.memoryForWake({
|
|
996
1098
|
sessionId: record.sessionId,
|
|
997
1099
|
ctx: record.ctx,
|
|
998
|
-
});
|
|
1100
|
+
}, record.event);
|
|
999
1101
|
// Same watchdog as a live wake, for the same reason: an unaborted Claude
|
|
1000
1102
|
// query outlives the promise that abandoned it and keeps spending.
|
|
1001
1103
|
const abort = new AbortController();
|
|
1104
|
+
const marks = WakeEffects.forWake(record.event, identity.sessionId);
|
|
1002
1105
|
let watchdog;
|
|
1003
1106
|
try {
|
|
1004
1107
|
const result = await Promise.race([
|
|
1005
|
-
this.brain.tick({
|
|
1108
|
+
this.wakeEffects.run(marks, () => this.brain.tick({
|
|
1006
1109
|
event: record.event,
|
|
1007
1110
|
ctx: record.ctx,
|
|
1008
1111
|
state: record.state,
|
|
@@ -1011,7 +1114,7 @@ export class Agent {
|
|
|
1011
1114
|
memory,
|
|
1012
1115
|
transitionTap: this.options.transitionTap,
|
|
1013
1116
|
signal: abort.signal,
|
|
1014
|
-
}),
|
|
1117
|
+
})),
|
|
1015
1118
|
new Promise((_, reject) => {
|
|
1016
1119
|
watchdog = setTimeout(() => {
|
|
1017
1120
|
abort.abort(new Error('tick timeout'));
|
|
@@ -1027,7 +1130,7 @@ export class Agent {
|
|
|
1027
1130
|
model: this.options.model ?? 'unknown',
|
|
1028
1131
|
cognition: this.options.cognition ?? 'fsm',
|
|
1029
1132
|
metrics: result.metrics,
|
|
1030
|
-
...
|
|
1133
|
+
...marks.scope,
|
|
1031
1134
|
});
|
|
1032
1135
|
}
|
|
1033
1136
|
catch (err) {
|
|
@@ -1091,7 +1194,7 @@ export class Agent {
|
|
|
1091
1194
|
// the agent assigned to itself.
|
|
1092
1195
|
onTask: async (task) => {
|
|
1093
1196
|
try {
|
|
1094
|
-
await this.wakeTaskAssignment(task.taskId);
|
|
1197
|
+
await this.wakeTaskAssignment(task.taskId, { fromInbox: true });
|
|
1095
1198
|
}
|
|
1096
1199
|
catch (err) {
|
|
1097
1200
|
runtimeLog.warn(this.options.name || 'agent', `inbox task ${task.taskId} could not be woken: ${err.message}`);
|
|
@@ -1129,7 +1232,7 @@ export class Agent {
|
|
|
1129
1232
|
});
|
|
1130
1233
|
}
|
|
1131
1234
|
/** replay task-assigned for an existing active task (demo recovery). */
|
|
1132
|
-
async wakeTaskAssignment(taskId) {
|
|
1235
|
+
async wakeTaskAssignment(taskId, options) {
|
|
1133
1236
|
const agentId = this.options.agentId;
|
|
1134
1237
|
if (!agentId || !this.taskService) {
|
|
1135
1238
|
throw new Error('wakeTaskAssignment: agent not configured');
|
|
@@ -1146,6 +1249,7 @@ export class Agent {
|
|
|
1146
1249
|
}
|
|
1147
1250
|
await this.eventQueue.enqueue({
|
|
1148
1251
|
type: 'task_result',
|
|
1252
|
+
...(options?.fromInbox ? { source: 'active-task-snapshot' } : {}),
|
|
1149
1253
|
result: {
|
|
1150
1254
|
...full,
|
|
1151
1255
|
taskId: full.taskId ?? taskId,
|
|
@@ -1159,8 +1263,9 @@ export class Agent {
|
|
|
1159
1263
|
this.started = false;
|
|
1160
1264
|
this.pollRunner?.stop();
|
|
1161
1265
|
this.stateTimers.clearAll();
|
|
1162
|
-
|
|
1266
|
+
const stopping = this.inboxLoop?.stop() ?? Promise.resolve();
|
|
1163
1267
|
this.inboxLoop = null;
|
|
1268
|
+
return stopping;
|
|
1164
1269
|
}
|
|
1165
1270
|
/**
|
|
1166
1271
|
* Waits for in-flight ticks to finish. Call before process.exit() on SIGTERM.
|
|
@@ -49,6 +49,14 @@ export interface ClaudeBrainOptions {
|
|
|
49
49
|
statelessPerWake?: boolean;
|
|
50
50
|
/** Injectable for tests. Defaults to @anthropic-ai/claude-agent-sdk query(). */
|
|
51
51
|
queryFn?: typeof query;
|
|
52
|
+
/** Full authoritative agreement read used to recognize listing bookkeeping. */
|
|
53
|
+
readAgreement?: (agreementId: string) => Promise<unknown>;
|
|
54
|
+
/**
|
|
55
|
+
* Opt in to spending a model turn on an activation notice for a hire
|
|
56
|
+
* this agent's counterparty formed. Default is off — the host records a
|
|
57
|
+
* no-op and waits for the task.
|
|
58
|
+
*/
|
|
59
|
+
actsOnHireActivation?: boolean;
|
|
52
60
|
}
|
|
53
61
|
/**
|
|
54
62
|
* "the transcript you asked me to resume is not here".
|
|
@@ -90,6 +98,8 @@ export declare class ClaudeBrain implements Brain {
|
|
|
90
98
|
private maxTurns;
|
|
91
99
|
private statelessPerWake;
|
|
92
100
|
private queryFn;
|
|
101
|
+
private readAgreement;
|
|
102
|
+
private actsOnHireActivation;
|
|
93
103
|
/**
|
|
94
104
|
* Per-lane tool context. One agent object serves every room, and a
|
|
95
105
|
* wake's query can still be running when another lane starts. A single
|
|
@@ -100,5 +110,5 @@ export declare class ClaudeBrain implements Brain {
|
|
|
100
110
|
private toolContextByLane;
|
|
101
111
|
private sdkMcpByLane;
|
|
102
112
|
constructor(opts: ClaudeBrainOptions);
|
|
103
|
-
tick({ event, ctx, state, identity, eff, signal }: TickInput): Promise<TickOutput>;
|
|
113
|
+
tick({ event, ctx, state, identity, eff, memory, signal }: TickInput): Promise<TickOutput>;
|
|
104
114
|
}
|