@ziggs-ai/agent-sdk 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/dist/Agent.d.ts +28 -7
- package/dist/Agent.js +189 -74
- 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 +54 -7
- package/dist/brain/claudeSdkProtocol.d.ts +1 -1
- package/dist/brain/claudeSdkProtocol.js +5 -2
- package/dist/brain/claudeSdkTools.d.ts +5 -2
- package/dist/brain/claudeSdkTools.js +39 -10
- 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 +17 -3
- 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/tasks/TaskService.d.ts +23 -1
- package/dist/platform/tasks/TaskService.js +8 -2
- package/dist/platform/tools/index.d.ts +3 -2
- package/dist/platform/tools/index.js +3 -2
- package/dist/platform/tools/tier1/protocolRunner.js +8 -1
- package/dist/platform/tools/tier1/protocolTools.js +3 -3
- package/dist/platform/tools/tier2/contextTools.d.ts +3 -0
- package/dist/platform/tools/tier2/contextTools.js +4 -1
- package/dist/platform/tools/tier2/planTools.d.ts +20 -0
- package/dist/platform/tools/tier2/planTools.js +64 -0
- package/dist/platform/tools/tier2/taskTools.d.ts +1 -0
- package/dist/platform/tools/tier2/taskTools.js +2 -1
- 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/dist/runtime/wakeOutcome.d.ts +33 -0
- package/dist/runtime/wakeOutcome.js +61 -6
- package/dist/types.d.ts +13 -0
- package/package.json +2 -2
- package/dist/pricing/fleetFreeTierA.gen.d.ts +0 -6
- package/dist/pricing/fleetFreeTierA.gen.js +0 -11
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,11 @@
|
|
|
12
12
|
|
|
13
13
|
## Unreleased
|
|
14
14
|
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- `task_cancel` on the hosted Claude surface, same PATCH /tasks/:id/cancel
|
|
18
|
+
as the MCP catalog verb. A held graph withdraws by cancelling the root.
|
|
19
|
+
|
|
15
20
|
### Breaking
|
|
16
21
|
|
|
17
22
|
- `AgentHostOptions.questMatchTags` is now `requestMatchTags`. The option is an
|
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,14 +17,14 @@ 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';
|
|
21
24
|
import { marketplaceViewTool } from './platform/tools/tier2/marketplaceTools.js';
|
|
22
25
|
import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
23
26
|
import { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
|
|
24
|
-
import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient, sendChatMessage, } from '@ziggs-ai/api-client';
|
|
27
|
+
import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient, sendChatMessage, presentSendResult, } from '@ziggs-ai/api-client';
|
|
25
28
|
import { normalizeNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
|
|
26
29
|
import { TaskService } from './platform/tasks/TaskService.js';
|
|
27
30
|
import { AgreementService } from './platform/agreements/AgreementService.js';
|
|
@@ -30,7 +33,8 @@ import { requestExactMatch } from './platform/requestMatch.js';
|
|
|
30
33
|
import { SeenMessages } from './platform/SeenMessages.js';
|
|
31
34
|
import { runtimeLog } from './shared/runtimeLog.js';
|
|
32
35
|
import { isTelemetryIngestEnabled, sendTelemetry, } from './platform/telemetryIngest.js';
|
|
33
|
-
import { buildWakeOutcomePayload, classifyWakeOutcome, } from './runtime/wakeOutcome.js';
|
|
36
|
+
import { buildWakeOutcomePayload, classifyWakeOutcome, wakeWasAsked, } 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,28 @@ 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: the server did not accept this message — nothing was delivered and nobody was woken. Retry with the same messageId, which the backend dedups on.');
|
|
320
|
+
this.outboundMessages.mark(JSON.stringify([result.chatId, result.messageId]));
|
|
321
|
+
// Presented once, here, so every hosted send — FSM reply, brain tool,
|
|
322
|
+
// stream follow-up — reports the destination the same way the MCP
|
|
323
|
+
// surface does.
|
|
324
|
+
return { ...result, ...presentSendResult(result, { creds, surface: 'sdk' }) };
|
|
325
|
+
};
|
|
328
326
|
this.taskService = services.taskService;
|
|
329
327
|
this.agreementService = services.agreementService;
|
|
330
328
|
this.telemetryClient = services.telemetryClient;
|
|
@@ -366,12 +364,18 @@ export class Agent {
|
|
|
366
364
|
agentId: this.options.agentId,
|
|
367
365
|
telemetryClient: services.telemetryClient,
|
|
368
366
|
telemetryIngest: this.options.telemetryIngest,
|
|
369
|
-
effectTap:
|
|
367
|
+
effectTap: (effect, response) => {
|
|
368
|
+
const marks = this.wakeEffects.getStore();
|
|
369
|
+
marks?.observe(effect, response);
|
|
370
|
+
for (const key of marks?.sentMessageKeys ?? [])
|
|
371
|
+
this.outboundMessages.mark(key);
|
|
372
|
+
this.options.effectTap?.(effect, response);
|
|
373
|
+
},
|
|
370
374
|
// `wake_at` reaches the lane's timer through here. The handler
|
|
371
375
|
// is built once and has no laneKey, so it derives one the same way the
|
|
372
376
|
// event queue does (see laneKeyFor).
|
|
373
377
|
wakeScheduler: {
|
|
374
|
-
armWakeAt: (sessionId, at, reason) => this.stateTimers.armWakeAt(laneKeyFor(this.options.agentId, sessionId), at, reason),
|
|
378
|
+
armWakeAt: (sessionId, at, reason) => this.stateTimers.armWakeAt(laneKeyFor(this.options.agentId, sessionId), at, reason, this.wakeEffects.getStore()?.scope),
|
|
375
379
|
},
|
|
376
380
|
});
|
|
377
381
|
this.brain = isClaudeCognition(this.options.cognition)
|
|
@@ -390,6 +394,7 @@ export class Agent {
|
|
|
390
394
|
// services from the effect handler's deps like the FSM path.
|
|
391
395
|
maxTurns: this.options.claudeMaxTurns,
|
|
392
396
|
statelessPerWake: this.options.claudeStatelessPerWake,
|
|
397
|
+
actsOnHireActivation: this.options.actsOnHireActivation === true,
|
|
393
398
|
})
|
|
394
399
|
: new ZiggsBrain({
|
|
395
400
|
workflow,
|
|
@@ -513,7 +518,7 @@ export class Agent {
|
|
|
513
518
|
// the same message being processed twice; leaving it set on a failed wake
|
|
514
519
|
// would make the redelivery this rejection buys us a no-op.
|
|
515
520
|
try {
|
|
516
|
-
await this.eventQueue.enqueue(normalized.event, laneKey);
|
|
521
|
+
await this.eventQueue.enqueue({ ...normalized.event, chatId: normalized.chatId }, laneKey);
|
|
517
522
|
}
|
|
518
523
|
catch (err) {
|
|
519
524
|
if (messageId)
|
|
@@ -542,7 +547,13 @@ export class Agent {
|
|
|
542
547
|
const agentId = session.ownAgentId || this.options.agentId;
|
|
543
548
|
const laneKey = laneKeyFor(agentId, session.sessionId);
|
|
544
549
|
runtimeLog.info(this.options.name || 'agent', `wake-outcome agent=${agentId} lane=${laneKey} kind=${outcome.kind}` +
|
|
545
|
-
(outcome.reason ? ` reason=${outcome.reason}` : '')
|
|
550
|
+
(outcome.reason ? ` reason=${outcome.reason}` : '') +
|
|
551
|
+
// A question nobody answered is the line worth grepping for, so it is
|
|
552
|
+
// on the line rather than only in the telemetry payload.
|
|
553
|
+
(outcome.reply
|
|
554
|
+
? ` asked=1 answered=${outcome.reply.sent ? 1 : 0}` +
|
|
555
|
+
(outcome.reply.sent ? '' : ` unanswered=${outcome.reply.reason}`)
|
|
556
|
+
: ''));
|
|
546
557
|
sendTelemetry(this.telemetryClient, buildWakeOutcomePayload({
|
|
547
558
|
agentId,
|
|
548
559
|
sessionId: session.sessionId,
|
|
@@ -551,12 +562,28 @@ export class Agent {
|
|
|
551
562
|
llmCalls,
|
|
552
563
|
}), isTelemetryIngestEnabled(this.options.telemetryIngest));
|
|
553
564
|
}
|
|
554
|
-
memoryForWake(session) {
|
|
555
|
-
|
|
565
|
+
memoryForWake(session, event) {
|
|
566
|
+
const fromEvent = hireIdsFromEvent(event);
|
|
567
|
+
const wakeAgreementIds = uniqueHireIds([
|
|
568
|
+
...fromEvent,
|
|
569
|
+
session.actingHireId,
|
|
570
|
+
]);
|
|
571
|
+
const section = memorySectionForWake({
|
|
556
572
|
sessionId: session.sessionId,
|
|
557
573
|
activeAgreementId: session.ctx
|
|
558
574
|
?.activeAgreementId,
|
|
559
|
-
|
|
575
|
+
wakeAgreementIds,
|
|
576
|
+
});
|
|
577
|
+
const known = uniqueHireIds([
|
|
578
|
+
session.ctx
|
|
579
|
+
?.activeAgreementId,
|
|
580
|
+
...wakeAgreementIds,
|
|
581
|
+
]);
|
|
582
|
+
if (known.length === 1)
|
|
583
|
+
session.actingHireId = known[0];
|
|
584
|
+
else if (known.length > 1)
|
|
585
|
+
session.actingHireId = null;
|
|
586
|
+
return partitionMemoryStore(this.memory, section);
|
|
560
587
|
}
|
|
561
588
|
/**
|
|
562
589
|
* A chat-lane session that still carries a cancelled hire from a previous
|
|
@@ -585,6 +612,24 @@ export class Agent {
|
|
|
585
612
|
// Fail open: a lookup error must not drop a live hire's session.
|
|
586
613
|
}
|
|
587
614
|
}
|
|
615
|
+
async formedByForHireActivation(event) {
|
|
616
|
+
const fromWake = formedByFromWake(event);
|
|
617
|
+
if (fromWake)
|
|
618
|
+
return fromWake;
|
|
619
|
+
const agreementId = agreementIdFromWake(event);
|
|
620
|
+
if (!agreementId || !this.agreementService)
|
|
621
|
+
return undefined;
|
|
622
|
+
try {
|
|
623
|
+
const agreement = await this.agreementService.get(agreementId);
|
|
624
|
+
const formedBy = agreement?.formedBy;
|
|
625
|
+
if (formedBy === 'agent' || formedBy === 'principal')
|
|
626
|
+
return formedBy;
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
// Fail open: a lookup error must not drop a person's store-claim wake.
|
|
630
|
+
}
|
|
631
|
+
return undefined;
|
|
632
|
+
}
|
|
588
633
|
async laneAgreementIsTerminal(sessionId) {
|
|
589
634
|
const agreementId = agreementIdFromLane(sessionId);
|
|
590
635
|
if (!agreementId || !this.agreementService)
|
|
@@ -598,6 +643,43 @@ export class Agent {
|
|
|
598
643
|
return false;
|
|
599
644
|
}
|
|
600
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* Whether the host answers this wake without spending a model turn.
|
|
648
|
+
*
|
|
649
|
+
* One decision, consulted by the live path and by {@link Agent.replayWake},
|
|
650
|
+
* because a recording is only worth keeping if a replay can tell you what the
|
|
651
|
+
* host would do now. It used to run only on the live path, so an edit to any
|
|
652
|
+
* of these rules was invisible to a replay — the tool that exists to answer
|
|
653
|
+
* "what changed?" could not see the change.
|
|
654
|
+
*
|
|
655
|
+
* Every rule here reads a row or the frame itself. None of them reads a lane,
|
|
656
|
+
* an agent kind, a name or an id shape.
|
|
657
|
+
*/
|
|
658
|
+
async wakeSuppression(event, sessionId) {
|
|
659
|
+
if (await this.laneAgreementIsTerminal(sessionId)) {
|
|
660
|
+
return { reason: 'agreement_terminal', because: 'its agreement is terminal' };
|
|
661
|
+
}
|
|
662
|
+
if (this.isSettledTaskEcho(event)) {
|
|
663
|
+
return {
|
|
664
|
+
reason: 'task_already_settled',
|
|
665
|
+
because: 'its task already reported a result',
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
if (isHireActivationNotice(event)) {
|
|
669
|
+
const formedBy = await this.formedByForHireActivation(event);
|
|
670
|
+
if (shouldSkipAgentFormedHireActivation({
|
|
671
|
+
event,
|
|
672
|
+
formedBy,
|
|
673
|
+
actsOnHireActivation: this.options.actsOnHireActivation === true,
|
|
674
|
+
})) {
|
|
675
|
+
return {
|
|
676
|
+
reason: 'agent_formed_hire_activation',
|
|
677
|
+
because: 'agent-formed hire activation',
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
601
683
|
/**
|
|
602
684
|
* the trailing turn on work that is already reported.
|
|
603
685
|
*
|
|
@@ -780,14 +862,30 @@ export class Agent {
|
|
|
780
862
|
const session = this.sessions.get(laneKey);
|
|
781
863
|
if (!session?.sessionId)
|
|
782
864
|
return;
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
865
|
+
// Presentation may call our sender an rpb-* room binding or the person we
|
|
866
|
+
// stand in for. Only an exact (chat, messageId) we successfully sent proves
|
|
867
|
+
// an echo; another message from the same binding still gets a full wake.
|
|
868
|
+
const incoming = event;
|
|
869
|
+
const events = incoming?.type === 'batch' && Array.isArray(incoming.events) ? incoming.events : [event];
|
|
870
|
+
const remaining = events.filter((item) => {
|
|
871
|
+
const e = item;
|
|
872
|
+
const task = e?.type === 'task_result' ? e.result : undefined;
|
|
873
|
+
if (this.stateTimers.peek(laneKey)?.kind === 'wake-at'
|
|
874
|
+
&& e?.source === 'active-task-snapshot' && (task?.state === 'active' || task?.state === 'in-progress')
|
|
875
|
+
&& task?.assigneeId === this.options.agentId && task.taskId && task.updatedAt && this.confirmedPlanVersions.has(JSON.stringify([laneKey, JSON.stringify([task.taskId, task.updatedAt])])))
|
|
876
|
+
return false;
|
|
877
|
+
return e?.type !== 'message' || !e.messageId || !this.outboundMessages.has(JSON.stringify([e.chatId ?? session.sessionId, e.messageId]));
|
|
878
|
+
});
|
|
879
|
+
if (remaining.length === 0) {
|
|
880
|
+
this.recordWakeOutcome(session, { kind: 'no-op', reason: events.some(e => e?.type === 'task_result') ? 'own_plan_snapshot' : 'own_message_echo' });
|
|
786
881
|
return;
|
|
787
882
|
}
|
|
788
|
-
if (
|
|
789
|
-
|
|
790
|
-
|
|
883
|
+
if (remaining.length !== events.length)
|
|
884
|
+
event = remaining.length === 1 ? remaining[0] : { type: 'batch', events: remaining };
|
|
885
|
+
const suppressed = await this.wakeSuppression(event, session.sessionId);
|
|
886
|
+
if (suppressed) {
|
|
887
|
+
runtimeLog.info(this.options.name || 'agent', `skipping wake on ${session.sessionId} — ${suppressed.because}`);
|
|
888
|
+
this.recordWakeOutcome(session, { kind: 'no-op', reason: suppressed.reason });
|
|
791
889
|
return;
|
|
792
890
|
}
|
|
793
891
|
// Before the tick spends anything: work this agent declared it cannot do
|
|
@@ -799,9 +897,10 @@ export class Agent {
|
|
|
799
897
|
const frame = this.stateTimers.stripStale(event, laneKey);
|
|
800
898
|
if (frame === null)
|
|
801
899
|
return;
|
|
802
|
-
//
|
|
803
|
-
//
|
|
804
|
-
|
|
900
|
+
// Traffic resets inactivity timers, but must not cancel an appointment
|
|
901
|
+
// the agent explicitly requested. Only its own fresh timer frame consumes
|
|
902
|
+
// that wake; a notice-only tick cannot ask the model to restore it.
|
|
903
|
+
this.stateTimers.consumeForEvent(frame, laneKey);
|
|
805
904
|
// Cold-start recovery: restore FSM state from sessionStore when the
|
|
806
905
|
// in-memory lane has no prior context (e.g. after a container restart).
|
|
807
906
|
if (this.sessionStore && session.ctx === undefined && session.state === undefined) {
|
|
@@ -822,7 +921,7 @@ export class Agent {
|
|
|
822
921
|
sessionId: session.sessionId,
|
|
823
922
|
laneKey,
|
|
824
923
|
};
|
|
825
|
-
const memory = this.memoryForWake(session);
|
|
924
|
+
const memory = this.memoryForWake(session, frame);
|
|
826
925
|
// Recorded here rather than after the tick: this is the last point where the
|
|
827
926
|
// input is still exactly what the brain will see, and a wake that goes on to
|
|
828
927
|
// throw is the one most worth replaying.
|
|
@@ -850,20 +949,17 @@ export class Agent {
|
|
|
850
949
|
// a zombie — burning tokens and racing the redelivered retry of the same
|
|
851
950
|
// event (duplicate sends, runaway per-wake input).
|
|
852
951
|
const tickAbort = new AbortController();
|
|
853
|
-
const marks =
|
|
952
|
+
const marks = WakeEffects.forWake(frame, session.sessionId);
|
|
854
953
|
const eff = async (effect) => {
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
marks.tasks += 1;
|
|
861
|
-
}
|
|
862
|
-
return this.eff(effect);
|
|
954
|
+
const response = await this.eff(effect);
|
|
955
|
+
marks.observe(effect, response);
|
|
956
|
+
for (const key of marks.sentMessageKeys)
|
|
957
|
+
this.outboundMessages.mark(key);
|
|
958
|
+
return response;
|
|
863
959
|
};
|
|
864
960
|
try {
|
|
865
961
|
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 }),
|
|
962
|
+
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
963
|
new Promise((_, reject) => {
|
|
868
964
|
tickWatchdog = setTimeout(() => {
|
|
869
965
|
tickAbort.abort(new Error('tick timeout'));
|
|
@@ -875,11 +971,11 @@ export class Agent {
|
|
|
875
971
|
session.state = result.state;
|
|
876
972
|
this.sessions.set(laneKey, session);
|
|
877
973
|
this.recordWakeOutcome(session, classifyWakeOutcome({
|
|
878
|
-
|
|
879
|
-
?.lastOutcome?.kind,
|
|
974
|
+
skippedReason: result.skippedReason,
|
|
880
975
|
llmCalls: result.metrics?.llmCalls ?? 0,
|
|
881
|
-
sentMessage: marks.
|
|
882
|
-
taskWrite: marks.
|
|
976
|
+
sentMessage: marks.sentMessage,
|
|
977
|
+
taskWrite: marks.taskWrite,
|
|
978
|
+
asked: wakeWasAsked(frame, identity.agentId),
|
|
883
979
|
}), result.metrics?.llmCalls ?? 0);
|
|
884
980
|
// One line per wake that actually spent LLM budget — the handle for
|
|
885
981
|
// tracking a hosted agent live (grep `agent=<id>`). Parked no-op ticks
|
|
@@ -902,7 +998,7 @@ export class Agent {
|
|
|
902
998
|
model: this.options.model ?? 'unknown',
|
|
903
999
|
cognition: this.options.cognition ?? 'fsm',
|
|
904
1000
|
metrics: result.metrics,
|
|
905
|
-
...
|
|
1001
|
+
...marks.scope,
|
|
906
1002
|
});
|
|
907
1003
|
}
|
|
908
1004
|
catch (err) {
|
|
@@ -934,6 +1030,14 @@ export class Agent {
|
|
|
934
1030
|
this.unmetTasksPendingAsk.delete(laneKey);
|
|
935
1031
|
this.askedForAccessLanes.delete(laneKey);
|
|
936
1032
|
}
|
|
1033
|
+
// The poll can return the row just written by this wake. Only suppress
|
|
1034
|
+
// that exact version while this lane has an explicit future wake: a
|
|
1035
|
+
// successful but unfinished turn still needs the task poll to recover
|
|
1036
|
+
// when it did not schedule its own continuation.
|
|
1037
|
+
if (this.stateTimers.peek(laneKey)?.kind === 'wake-at') {
|
|
1038
|
+
for (const version of marks.planVersions)
|
|
1039
|
+
this.confirmedPlanVersions.mark(JSON.stringify([laneKey, version]));
|
|
1040
|
+
}
|
|
937
1041
|
}
|
|
938
1042
|
catch (err) {
|
|
939
1043
|
this.recordWakeOutcome(session, {
|
|
@@ -992,17 +1096,26 @@ export class Agent {
|
|
|
992
1096
|
sessionId: record.sessionId,
|
|
993
1097
|
laneKey: record.laneKey || record.sessionId,
|
|
994
1098
|
};
|
|
1099
|
+
// The host's own answer comes first, exactly as it does live. A replay that
|
|
1100
|
+
// skipped this would report a model turn the host would never have spent,
|
|
1101
|
+
// which is the opposite of what a recording is for.
|
|
1102
|
+
const suppressed = await this.wakeSuppression(record.event, record.sessionId);
|
|
1103
|
+
if (suppressed) {
|
|
1104
|
+
runtimeLog.info(this.options.name || 'agent', `replay: no wake on ${record.sessionId} — ${suppressed.because}`);
|
|
1105
|
+
return { state: record.state };
|
|
1106
|
+
}
|
|
995
1107
|
const memory = this.memoryForWake({
|
|
996
1108
|
sessionId: record.sessionId,
|
|
997
1109
|
ctx: record.ctx,
|
|
998
|
-
});
|
|
1110
|
+
}, record.event);
|
|
999
1111
|
// Same watchdog as a live wake, for the same reason: an unaborted Claude
|
|
1000
1112
|
// query outlives the promise that abandoned it and keeps spending.
|
|
1001
1113
|
const abort = new AbortController();
|
|
1114
|
+
const marks = WakeEffects.forWake(record.event, identity.sessionId);
|
|
1002
1115
|
let watchdog;
|
|
1003
1116
|
try {
|
|
1004
1117
|
const result = await Promise.race([
|
|
1005
|
-
this.brain.tick({
|
|
1118
|
+
this.wakeEffects.run(marks, () => this.brain.tick({
|
|
1006
1119
|
event: record.event,
|
|
1007
1120
|
ctx: record.ctx,
|
|
1008
1121
|
state: record.state,
|
|
@@ -1011,7 +1124,7 @@ export class Agent {
|
|
|
1011
1124
|
memory,
|
|
1012
1125
|
transitionTap: this.options.transitionTap,
|
|
1013
1126
|
signal: abort.signal,
|
|
1014
|
-
}),
|
|
1127
|
+
})),
|
|
1015
1128
|
new Promise((_, reject) => {
|
|
1016
1129
|
watchdog = setTimeout(() => {
|
|
1017
1130
|
abort.abort(new Error('tick timeout'));
|
|
@@ -1027,7 +1140,7 @@ export class Agent {
|
|
|
1027
1140
|
model: this.options.model ?? 'unknown',
|
|
1028
1141
|
cognition: this.options.cognition ?? 'fsm',
|
|
1029
1142
|
metrics: result.metrics,
|
|
1030
|
-
...
|
|
1143
|
+
...marks.scope,
|
|
1031
1144
|
});
|
|
1032
1145
|
}
|
|
1033
1146
|
catch (err) {
|
|
@@ -1091,7 +1204,7 @@ export class Agent {
|
|
|
1091
1204
|
// the agent assigned to itself.
|
|
1092
1205
|
onTask: async (task) => {
|
|
1093
1206
|
try {
|
|
1094
|
-
await this.wakeTaskAssignment(task.taskId);
|
|
1207
|
+
await this.wakeTaskAssignment(task.taskId, { fromInbox: true });
|
|
1095
1208
|
}
|
|
1096
1209
|
catch (err) {
|
|
1097
1210
|
runtimeLog.warn(this.options.name || 'agent', `inbox task ${task.taskId} could not be woken: ${err.message}`);
|
|
@@ -1129,7 +1242,7 @@ export class Agent {
|
|
|
1129
1242
|
});
|
|
1130
1243
|
}
|
|
1131
1244
|
/** replay task-assigned for an existing active task (demo recovery). */
|
|
1132
|
-
async wakeTaskAssignment(taskId) {
|
|
1245
|
+
async wakeTaskAssignment(taskId, options) {
|
|
1133
1246
|
const agentId = this.options.agentId;
|
|
1134
1247
|
if (!agentId || !this.taskService) {
|
|
1135
1248
|
throw new Error('wakeTaskAssignment: agent not configured');
|
|
@@ -1146,6 +1259,7 @@ export class Agent {
|
|
|
1146
1259
|
}
|
|
1147
1260
|
await this.eventQueue.enqueue({
|
|
1148
1261
|
type: 'task_result',
|
|
1262
|
+
...(options?.fromInbox ? { source: 'active-task-snapshot' } : {}),
|
|
1149
1263
|
result: {
|
|
1150
1264
|
...full,
|
|
1151
1265
|
taskId: full.taskId ?? taskId,
|
|
@@ -1159,8 +1273,9 @@ export class Agent {
|
|
|
1159
1273
|
this.started = false;
|
|
1160
1274
|
this.pollRunner?.stop();
|
|
1161
1275
|
this.stateTimers.clearAll();
|
|
1162
|
-
|
|
1276
|
+
const stopping = this.inboxLoop?.stop() ?? Promise.resolve();
|
|
1163
1277
|
this.inboxLoop = null;
|
|
1278
|
+
return stopping;
|
|
1164
1279
|
}
|
|
1165
1280
|
/**
|
|
1166
1281
|
* 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
|
}
|