@ziggs-ai/agent-sdk 0.42.0 → 0.43.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 +40 -0
- package/README.md +1 -1
- package/dist/Agent.d.ts +27 -8
- package/dist/Agent.js +188 -98
- package/dist/brain/ClaudeBrain.d.ts +0 -1
- package/dist/brain/ClaudeBrain.js +18 -42
- package/dist/brain/claudeCognition.js +5 -6
- package/dist/brain/claudeSdkProtocol.js +9 -9
- package/dist/brain/claudeSdkTools.d.ts +5 -5
- package/dist/brain/claudeSdkTools.js +40 -135
- package/dist/brain/hostedToolSurface.js +1 -1
- package/dist/brain/standingOfferNotice.d.ts +0 -2
- package/dist/brain/standingOfferNotice.js +1 -21
- package/dist/brain/toolManagerToSdkMcp.js +1 -1
- package/dist/brain/wakeGate.d.ts +3 -1
- package/dist/brain/wakeGate.js +6 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/ingress/normalizeIncoming.d.ts +5 -3
- package/dist/ingress/normalizeIncoming.js +9 -8
- package/dist/mcp/createMcpAgent.d.ts +15 -0
- package/dist/mcp/createMcpAgent.js +47 -11
- package/dist/memory/MemoryStore.d.ts +1 -1
- package/dist/memory/MemoryStore.js +1 -6
- package/dist/platform/ConnectionPool.js +1 -1
- package/dist/platform/OutboxBuffer.d.ts +2 -2
- package/dist/platform/OutboxBuffer.js +17 -17
- package/dist/platform/StateTimers.d.ts +3 -1
- package/dist/platform/StateTimers.js +4 -3
- package/dist/platform/WakeLedger.d.ts +10 -6
- package/dist/platform/WakeLedger.js +16 -9
- package/dist/platform/ZiggsEffectHandler.d.ts +18 -4
- package/dist/platform/ZiggsEffectHandler.js +86 -55
- package/dist/platform/agreements/AgreementService.d.ts +1 -3
- package/dist/platform/agreements/AgreementService.js +1 -7
- package/dist/platform/inboxLoop.d.ts +5 -9
- package/dist/platform/inboxLoop.js +165 -158
- package/dist/platform/polls/PollKind.d.ts +13 -16
- package/dist/platform/polls/PollKind.js +4 -4
- package/dist/platform/polls/PollRunner.d.ts +3 -20
- package/dist/platform/polls/PollRunner.js +13 -15
- package/dist/platform/polls/declaredPollRunner.d.ts +1 -1
- package/dist/platform/polls/declaredPollRunner.js +1 -1
- package/dist/platform/tasks/TaskService.d.ts +4 -3
- package/dist/platform/tasks/TaskService.js +1 -4
- package/dist/platform/telemetryIngest.js +4 -4
- package/dist/platform/tools/index.d.ts +1 -2
- package/dist/platform/tools/index.js +1 -4
- package/dist/platform/tools/tier1/protocolRunner.d.ts +1 -1
- package/dist/platform/tools/tier1/protocolRunner.js +24 -17
- package/dist/platform/tools/tier1/protocolTools.js +2 -2
- package/dist/platform/tools/tier1/wakeTools.d.ts +1 -1
- package/dist/platform/tools/tier1/wakeTools.js +2 -1
- package/dist/platform/tools/tier2/mcpConnectionTools.js +1 -1
- package/dist/platform/tools/tier2/taskTools.d.ts +1 -0
- package/dist/platform/tools/tier2/taskTools.js +2 -1
- package/dist/runtime/AgentMachine.d.ts +2 -2
- package/dist/runtime/AgentMachine.js +4 -5
- package/dist/runtime/PromptBuilder.d.ts +1 -1
- package/dist/runtime/PromptBuilder.js +5 -4
- package/dist/runtime/buildOutcome.d.ts +1 -1
- package/dist/runtime/deliveryWriteKeys.d.ts +1 -1
- package/dist/runtime/deliveryWriteKeys.js +1 -1
- package/dist/runtime/kickoff.d.ts +14 -43
- package/dist/runtime/kickoff.js +22 -178
- package/dist/runtime/messageCommand.d.ts +9 -0
- package/dist/runtime/messageCommand.js +23 -0
- package/dist/runtime/requiredContext.d.ts +6 -0
- package/dist/runtime/requiredContext.js +23 -0
- package/dist/runtime/runTurn.js +31 -106
- package/dist/runtime/taskIntake.js +8 -22
- package/dist/runtime/wakeEffects.d.ts +3 -12
- package/dist/runtime/wakeEffects.js +17 -48
- package/dist/runtime/wakeOutcome.js +1 -1
- package/dist/skills/createSkillAgent.js +12 -1
- package/dist/tools/capabilityAdapter.js +1 -1
- package/dist/tools/recordReport.d.ts +1 -1
- package/dist/tools/recordReport.js +3 -3
- package/dist/tools/toolCreds.d.ts +1 -1
- package/dist/tools/toolCreds.js +4 -2
- package/dist/types.d.ts +17 -27
- package/package.json +3 -3
- package/dist/platform/tools/tier2/chatTools.d.ts +0 -3
- package/dist/platform/tools/tier2/chatTools.js +0 -7
- package/dist/probeChat.d.ts +0 -10
- package/dist/probeChat.js +0 -12
package/dist/Agent.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { runWorkExecution } from '@ziggs-ai/api-client';
|
|
2
|
+
import { MessageRecordsClient } from '@ziggs-ai/api-client/http';
|
|
3
|
+
import { responseCommand } from './runtime/messageCommand.js';
|
|
1
4
|
import { validateWorkExecution, withWorkExecution, currentWorkExecution } from '@ziggs-ai/api-client';
|
|
2
5
|
import { SERVER_EVENTS } from '@ziggs-ai/contracts';
|
|
3
6
|
import { withAgreementContext } from './runtime/agreementContext.js';
|
|
4
|
-
import { agreementLaneId,
|
|
7
|
+
import { agreementLaneId, isContextAccessRequest } from '@ziggs-ai/api-client';
|
|
5
8
|
import { createDeclaredPollRunner } from './platform/polls/declaredPollRunner.js';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
7
9
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
8
10
|
import { contextScopesFromWake, WakeEffects, workScopeFromWake } from './runtime/wakeEffects.js';
|
|
9
11
|
import { ExecutionController, asWorkTool, executionControllers } from './runtime/executionController.js';
|
|
@@ -16,7 +18,6 @@ import { CLAUDE_SDK_TOOLS } from './brain/claudeSdkTools.js';
|
|
|
16
18
|
import { resolveClaudeModel } from './brain/claudeCognition.js';
|
|
17
19
|
import { resolveFleetModel } from './shared/fleetModel.js';
|
|
18
20
|
export { DEFAULT_MODEL, resolveFleetModel } from './shared/fleetModel.js';
|
|
19
|
-
import { CHAT_TOOLS } from './platform/tools/tier2/chatTools.js';
|
|
20
21
|
import { contextReadTool, openTool, accessExplainTool } from './platform/tools/tier2/contextTools.js';
|
|
21
22
|
import { EventQueue } from './platform/EventQueue.js';
|
|
22
23
|
import { StateTimers } from './platform/StateTimers.js';
|
|
@@ -35,7 +36,7 @@ import { PROTOCOL_TOOLS } from './platform/tools/tier1/protocolTools.js';
|
|
|
35
36
|
import { marketplaceViewTool } from './platform/tools/tier2/marketplaceTools.js';
|
|
36
37
|
import { OpenAIAdapter } from './adapters/OpenAIAdapter.js';
|
|
37
38
|
import { AnthropicAdapter } from './adapters/AnthropicAdapter.js';
|
|
38
|
-
import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient,
|
|
39
|
+
import { ArtifactsClient, ConnectionsClient, ContextReadClient, TelemetryClient, getAgreement, getTask, annotationForSdkTool, } from '@ziggs-ai/api-client';
|
|
39
40
|
import { isCheckoutNeed, normalizeNeeds, unmetNeeds, unmetNeedsMessage, } from './tools/needs.js';
|
|
40
41
|
import { openCheckout, closeCheckout } from './checkout/open.js';
|
|
41
42
|
import { resolveBackendUrl } from './shared/backendUrl.js';
|
|
@@ -56,20 +57,16 @@ async function loadConnectionPool() {
|
|
|
56
57
|
const mod = await import(/* @vite-ignore */ modPath);
|
|
57
58
|
return mod.ConnectionPool;
|
|
58
59
|
}
|
|
59
|
-
const DEFAULT_SESSION_RESOLVER = (normalized) => normalized.
|
|
60
|
+
const DEFAULT_SESSION_RESOLVER = (normalized) => normalized.addressId ?? (normalized.event.agreementId ? `agrn-${normalized.event.agreementId}` : normalized.event.messageId ? `msgn-${normalized.event.messageId}` : null);
|
|
60
61
|
/**
|
|
61
62
|
* one lane key per unit of work, derived the same way as inbox wakes.
|
|
62
63
|
*
|
|
63
|
-
* Inbox deliveries
|
|
64
|
+
* Inbox deliveries use their room when present. Bootstrap used to
|
|
64
65
|
* prefer bare `agreementId`, which opened a second lane for the same task and
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* notification chat (`agrn-<agreementId>`), which is always linked after.
|
|
66
|
+
* misdirected the snapshot read. Use the agreement's local processing key. An origin link is
|
|
67
|
+
* navigation, not a delivery destination. This key creates no chat or grant.
|
|
68
68
|
*/
|
|
69
69
|
export function laneSessionIdForTask(task) {
|
|
70
|
-
if (typeof task.originChatId === 'string' && task.originChatId) {
|
|
71
|
-
return task.originChatId;
|
|
72
|
-
}
|
|
73
70
|
if (typeof task.agreementId === 'string' && task.agreementId) {
|
|
74
71
|
return agreementLaneId(task.agreementId);
|
|
75
72
|
}
|
|
@@ -86,7 +83,7 @@ const CHECKOUT_TICK_TIMEOUT_MS = Number(process.env.CLAUDE_SDK_CHECKOUT_TICK_TIM
|
|
|
86
83
|
* Anything else is a custom tool, and under execution control a custom tool
|
|
87
84
|
* does work: it gets a `taskId` the model fills (see asWorkTool).
|
|
88
85
|
*/
|
|
89
|
-
const HAND_DEFINED_PLATFORM_TOOLS = new Set([...PROTOCOL_TOOLS, ...
|
|
86
|
+
const HAND_DEFINED_PLATFORM_TOOLS = new Set([...PROTOCOL_TOOLS, ...CLAUDE_SDK_TOOLS, ...WAKE_TOOLS, marketplaceViewTool, contextReadTool, openTool, accessExplainTool]
|
|
90
87
|
.map((tool) => toolName(tool)));
|
|
91
88
|
function customToolAsWork(tool) {
|
|
92
89
|
const name = toolName(tool);
|
|
@@ -111,9 +108,6 @@ function createToolManager(options) {
|
|
|
111
108
|
// Both directions: shedding claim (taskTools array without it) sheds browse.
|
|
112
109
|
const canClaim = taskTools.some((t) => t.schema.function.name === 'agreement_claim');
|
|
113
110
|
const browseTools = canClaim ? [marketplaceViewTool] : [];
|
|
114
|
-
// chat_open is always-on so claim/provider kickoff (FSM) and the
|
|
115
|
-
// Claude always-on set can open the buyer chat after a store claim. CHAT_TOOLS
|
|
116
|
-
// is also folded into CLAUDE_SDK_TOOLS; Map registration makes duplicates a no-op.
|
|
117
111
|
// wake_at stays registered for the FSM and for the hosted model, where it is
|
|
118
112
|
// deferred behind tool search; an agent that can be handed a task
|
|
119
113
|
// with a check-back rule but cannot arm a wake would have to busy-wait, and
|
|
@@ -126,7 +120,7 @@ function createToolManager(options) {
|
|
|
126
120
|
const everydayContext = isClaudeCognition(options.cognition)
|
|
127
121
|
? [contextReadTool, openTool, accessExplainTool].filter((t) => !ownNames.has(toolName(t)))
|
|
128
122
|
: [];
|
|
129
|
-
const platformTools = [...taskTools, ...
|
|
123
|
+
const platformTools = [...taskTools, ...browseTools, ...claudeSdkTools, ...everydayContext, ...WAKE_TOOLS];
|
|
130
124
|
warnOnShadowedPlatformTools(platformTools, ownTools, options.agentId);
|
|
131
125
|
const controlOn = options.executionControl ?? isClaudeCognition(options.cognition);
|
|
132
126
|
toolManager.registerAll([...platformTools, ...(controlOn ? ownTools.map(customToolAsWork) : ownTools)]);
|
|
@@ -241,6 +235,11 @@ export class Agent {
|
|
|
241
235
|
/** Tasks left active by the asking turn, failed right after it (per lane). */
|
|
242
236
|
unmetTasksPendingAsk = new Map();
|
|
243
237
|
wakeEffects = new AsyncLocalStorage();
|
|
238
|
+
/**
|
|
239
|
+
* The roomless lane a live wake runs on. A tool's send or read reaches the
|
|
240
|
+
* host without the wake's effect wrapper, so both look the lane up here.
|
|
241
|
+
*/
|
|
242
|
+
wakeDestination = new AsyncLocalStorage();
|
|
244
243
|
outboundMessages = new SeenMessages();
|
|
245
244
|
confirmedPlanVersions = new SeenMessages();
|
|
246
245
|
grantProviderCache = null;
|
|
@@ -312,7 +311,7 @@ export class Agent {
|
|
|
312
311
|
this.sessionStore = options.sessionStore ?? null;
|
|
313
312
|
this._validate();
|
|
314
313
|
this.pollRunner = createDeclaredPollRunner(this.options, {
|
|
315
|
-
|
|
314
|
+
wakeTask: (_agentId, taskId) => this.wakeTaskAssignment(taskId),
|
|
316
315
|
});
|
|
317
316
|
this._setup();
|
|
318
317
|
}
|
|
@@ -344,7 +343,7 @@ export class Agent {
|
|
|
344
343
|
this.toolManager = services.toolManager;
|
|
345
344
|
this.llm = services.llm;
|
|
346
345
|
// Outbound send is HTTP (delivery law, phase 4): the agent no longer holds a
|
|
347
|
-
// socket. `POST /
|
|
346
|
+
// socket. `POST /messaging/messages` persists the whole message — the same
|
|
348
347
|
// row the WS path persisted. Live-token streaming had no HTTP path, so
|
|
349
348
|
// `chunkSender` is intentionally dropped: partials degrade, the full reply
|
|
350
349
|
// is never lost (runTurn always follows a stream with a whole send-message).
|
|
@@ -352,32 +351,7 @@ export class Agent {
|
|
|
352
351
|
operatorKey: this.options.operatorKey,
|
|
353
352
|
agentId: this.options.agentId,
|
|
354
353
|
};
|
|
355
|
-
const messageSender =
|
|
356
|
-
const sentId = messageId ?? `sdk_${randomUUID()}`;
|
|
357
|
-
const result = await sendChatMessage({
|
|
358
|
-
chatId: sessionId,
|
|
359
|
-
// Omitted on purpose when the caller has no valid address for THIS
|
|
360
|
-
// room: the backend infers the recipient from the room's own
|
|
361
|
-
// participants.
|
|
362
|
-
receiverId,
|
|
363
|
-
text: message,
|
|
364
|
-
// messageId is the backend's (chatId, messageId) dedup key. Callers
|
|
365
|
-
// pass a wake-derived id for replay-stability; fall back to a fresh id
|
|
366
|
-
// only when absent (matches the old WS path letting the server assign).
|
|
367
|
-
messageId: sentId,
|
|
368
|
-
conversationAgreementId,
|
|
369
|
-
underAgreementId,
|
|
370
|
-
taskId,
|
|
371
|
-
...(sources ? { sources } : {}),
|
|
372
|
-
}, creds);
|
|
373
|
-
if (!result.success)
|
|
374
|
-
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.');
|
|
375
|
-
this.outboundMessages.mark(JSON.stringify([result.chatId, result.messageId]));
|
|
376
|
-
// Presented once, here, so every hosted send — FSM reply, brain tool,
|
|
377
|
-
// stream follow-up — reports the destination the same way the MCP
|
|
378
|
-
// surface does.
|
|
379
|
-
return { ...result, ...presentSendResult(result, { creds, surface: 'sdk' }) };
|
|
380
|
-
};
|
|
354
|
+
const messageSender = (command) => new MessageRecordsClient(creds.operatorKey, creds.agentId, resolveBackendUrl()).send(command);
|
|
381
355
|
this.taskService = services.taskService;
|
|
382
356
|
this.agreementService = services.agreementService;
|
|
383
357
|
this.telemetryClient = services.telemetryClient;
|
|
@@ -413,6 +387,10 @@ export class Agent {
|
|
|
413
387
|
taskService: services.taskService,
|
|
414
388
|
agreementService: services.agreementService,
|
|
415
389
|
messageSender,
|
|
390
|
+
conversationFor: (sessionId) => {
|
|
391
|
+
const lane = this.wakeDestination.getStore();
|
|
392
|
+
return lane?.sessionId === sessionId ? lane : undefined;
|
|
393
|
+
},
|
|
416
394
|
toolManager: services.toolManager,
|
|
417
395
|
operatorKey: this.options.operatorKey,
|
|
418
396
|
agentId: this.options.agentId,
|
|
@@ -467,17 +445,17 @@ export class Agent {
|
|
|
467
445
|
promptBuilder: services.promptBuilder,
|
|
468
446
|
});
|
|
469
447
|
this.eventQueue = new EventQueue((event, laneKey) => this._processEvent(event, laneKey));
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
448
|
+
// Timer dispatch is at-least-once. Only successful work checkpoints this
|
|
449
|
+
// exact wake; a crash/failure leaves it durable, and an older completion
|
|
450
|
+
// cannot consume a timer armed while that work was running.
|
|
473
451
|
this.stateTimers = new StateTimers((ev, laneKey) => {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
void this.eventQueue.enqueue(ev, laneKey).catch((err) => {
|
|
452
|
+
const work = this.eventQueue.enqueue(ev, laneKey).then(() => {
|
|
453
|
+
if (ev.type === 'wake-at')
|
|
454
|
+
this.wakeLedger.done(laneKey, ev.wakeId);
|
|
455
|
+
}).catch((err) => {
|
|
479
456
|
runtimeLog.warn('Agent', `state-timer frame failed laneKey=${laneKey}: ${err?.message || err}`);
|
|
480
457
|
});
|
|
458
|
+
this.wakeLedger.track(work);
|
|
481
459
|
});
|
|
482
460
|
}
|
|
483
461
|
executionControlOn() {
|
|
@@ -522,8 +500,10 @@ export class Agent {
|
|
|
522
500
|
if (!this.taskService)
|
|
523
501
|
return null;
|
|
524
502
|
let task;
|
|
503
|
+
// Host control observation is separate from task work. Only execution state
|
|
504
|
+
// leaves this method; a stopped work context cannot observe its own stop.
|
|
525
505
|
try {
|
|
526
|
-
task = await this.taskService.getTask(taskId);
|
|
506
|
+
task = await withWorkExecution(undefined, () => this.taskService.getTask(taskId));
|
|
527
507
|
}
|
|
528
508
|
catch (error) {
|
|
529
509
|
// Unreadable is not a hold. The wake filter still drops a frame whose
|
|
@@ -537,6 +517,17 @@ export class Agent {
|
|
|
537
517
|
return null;
|
|
538
518
|
return { blocked: task.state === 'cancelled' || (task.state === 'active' && task.control?.action === 'pause'), closed: task.state === 'cancelled', control: task.control };
|
|
539
519
|
}
|
|
520
|
+
/**
|
|
521
|
+
* Control on an assignment the inbox will not run yet. No wake follows, so a
|
|
522
|
+
* resume is acknowledged here too once no run of the task is live; the wake
|
|
523
|
+
* path that would otherwise answer it never comes while the delivery is blocked.
|
|
524
|
+
*/
|
|
525
|
+
async observeBlockedTaskControl(taskId) {
|
|
526
|
+
const execution = await this.taskExecutionControl(taskId);
|
|
527
|
+
const idleResume = execution?.control?.action === 'resume' && !this.activeTaskRuns.get(taskId)?.size;
|
|
528
|
+
if (execution?.blocked || idleResume)
|
|
529
|
+
await this.acknowledgeExecutionControl(taskId, execution?.control);
|
|
530
|
+
}
|
|
540
531
|
async acknowledgeExecutionControl(taskId, control) {
|
|
541
532
|
if (control?.status !== 'requested')
|
|
542
533
|
return;
|
|
@@ -605,15 +596,15 @@ export class Agent {
|
|
|
605
596
|
}
|
|
606
597
|
// Map the event to a candidate session key using the configured resolver
|
|
607
598
|
// on a synthetic normalized-event shape. Default resolver buckets by
|
|
608
|
-
//
|
|
599
|
+
// addressId; agents using a different sessionResolver (agreementId, taskId,
|
|
609
600
|
// counterpartyId) will get the right lane here as long as the wire
|
|
610
601
|
// event carries the corresponding id.
|
|
611
602
|
const fakeNormalized = {
|
|
612
|
-
|
|
603
|
+
addressId: event.addressId,
|
|
613
604
|
event: { type: 'message', timestamp: Date.now(), text: '' },
|
|
614
605
|
};
|
|
615
606
|
let sessionId = this.sessionResolver(fakeNormalized, ownAgentId);
|
|
616
|
-
// store claims deliver agreement wakes with no
|
|
607
|
+
// store claims deliver agreement wakes with no addressId. Do not
|
|
617
608
|
// drop them; lane by agreement the same way task bootstrap does
|
|
618
609
|
// (`agrn-<agreementId>`), never a bare agreement id.
|
|
619
610
|
if (!sessionId) {
|
|
@@ -641,10 +632,10 @@ export class Agent {
|
|
|
641
632
|
return this.eventQueue.enqueue({
|
|
642
633
|
type: SERVER_EVENTS.resourceChanged,
|
|
643
634
|
resource: event,
|
|
644
|
-
|
|
635
|
+
addressId: event.addressId,
|
|
645
636
|
taskId: event.taskId,
|
|
646
637
|
agreementId,
|
|
647
|
-
// Claimer/buyer —
|
|
638
|
+
// Claimer/buyer — the canonical reply command explicitly names the requester.
|
|
648
639
|
...(actorId ? { senderId: actorId } : {}),
|
|
649
640
|
}, laneKey);
|
|
650
641
|
}
|
|
@@ -658,15 +649,27 @@ export class Agent {
|
|
|
658
649
|
['proposal_approved', 'proposal_rejected'].includes(notice?.operation ?? '') &&
|
|
659
650
|
isContextAccessRequest(notice?.agreement?.grantRequest))
|
|
660
651
|
return;
|
|
661
|
-
const messageId = (metadata.
|
|
652
|
+
const messageId = (metadata.receiptId ?? metadata.messageId);
|
|
662
653
|
if (messageId && this.seenMessages.has(messageId)) {
|
|
663
654
|
runtimeLog.debug('Agent', `skip duplicate messageId=${messageId}`);
|
|
664
655
|
return;
|
|
665
656
|
}
|
|
666
657
|
if (messageId)
|
|
667
658
|
this.seenMessages.mark(messageId);
|
|
659
|
+
// The inbox marks a message that has no room. It has no other way back to
|
|
660
|
+
// the agent, so one that cannot be routed here stays pending, never dropped.
|
|
661
|
+
const roomless = metadata.roomless === true && !metadata.addressId;
|
|
662
|
+
const roomlessAgreementId = metadata.roomless === true && !metadata.addressId
|
|
663
|
+
&& typeof metadata.agreementId === 'string' && metadata.agreementId ? metadata.agreementId : undefined;
|
|
664
|
+
const unroutable = (why) => {
|
|
665
|
+
if (messageId)
|
|
666
|
+
this.seenMessages.unmark(messageId);
|
|
667
|
+
return new Error(`handleMessage: message ${messageId ?? '(no id)'} ${roomlessAgreementId ? `under agreement ${roomlessAgreementId}` : 'with an exact action'} has no address and ${why}. Left pending.`);
|
|
668
|
+
};
|
|
668
669
|
const ownAgentId = this._resolveOwnAgentId();
|
|
669
670
|
if (!ownAgentId) {
|
|
671
|
+
if (roomless)
|
|
672
|
+
throw unroutable('ownAgentId is missing');
|
|
670
673
|
runtimeLog.warn('Agent', 'handleMessage: ownAgentId missing — dropping event');
|
|
671
674
|
return;
|
|
672
675
|
}
|
|
@@ -687,7 +690,9 @@ export class Agent {
|
|
|
687
690
|
const normalized = normalizeIncomingEvent({ text: enrichedText, metadata, ownAgentId });
|
|
688
691
|
const sessionId = this.sessionResolver(normalized, ownAgentId);
|
|
689
692
|
if (!sessionId) {
|
|
690
|
-
|
|
693
|
+
if (roomless)
|
|
694
|
+
throw unroutable('the session resolver returned no lane for it');
|
|
695
|
+
runtimeLog.warn('Agent', `session resolver returned null ownAgentId=${ownAgentId} addressId=${normalized.addressId ?? 'null'}`);
|
|
691
696
|
return;
|
|
692
697
|
}
|
|
693
698
|
const executionContext = metadata.executionContext;
|
|
@@ -695,6 +700,8 @@ export class Agent {
|
|
|
695
700
|
if (!this.sessions.has(laneKey)) {
|
|
696
701
|
this.sessions.set(laneKey, { sessionId, ownAgentId, executionContext });
|
|
697
702
|
}
|
|
703
|
+
if (roomlessAgreementId)
|
|
704
|
+
this.sessions.get(laneKey).roomlessAgreementId = roomlessAgreementId;
|
|
698
705
|
// The inbox loop owns ack (act-before-ack, delivery law); the agent only
|
|
699
706
|
// routes the event onto its lane. No live-path watermark ack — there is no
|
|
700
707
|
// separate live/push channel to reconcile against anymore.
|
|
@@ -703,7 +710,7 @@ export class Agent {
|
|
|
703
710
|
// the same message being processed twice; leaving it set on a failed wake
|
|
704
711
|
// would make the redelivery this rejection buys us a no-op.
|
|
705
712
|
try {
|
|
706
|
-
await this.eventQueue.enqueue({ ...normalized.event,
|
|
713
|
+
await this.eventQueue.enqueue({ ...normalized.event, addressId: normalized.addressId }, laneKey);
|
|
707
714
|
}
|
|
708
715
|
catch (err) {
|
|
709
716
|
if (messageId)
|
|
@@ -797,8 +804,7 @@ export class Agent {
|
|
|
797
804
|
// Fail open: a lookup error must not drop a live hire's session.
|
|
798
805
|
}
|
|
799
806
|
}
|
|
800
|
-
async
|
|
801
|
-
const agreementId = agreementIdFromLane(sessionId);
|
|
807
|
+
async agreementIsTerminal(agreementId) {
|
|
802
808
|
if (!agreementId || !this.agreementService)
|
|
803
809
|
return false;
|
|
804
810
|
try {
|
|
@@ -822,8 +828,10 @@ export class Agent {
|
|
|
822
828
|
* Every rule here reads a row or the frame itself. None of them reads a lane,
|
|
823
829
|
* an agent kind, a name or an id shape.
|
|
824
830
|
*/
|
|
825
|
-
async wakeSuppression(event,
|
|
826
|
-
|
|
831
|
+
async wakeSuppression(event, _sessionId) {
|
|
832
|
+
const execution = currentWorkExecution();
|
|
833
|
+
const agreementId = execution ? execution.agreementId : workScopeFromWake(event).agreementId;
|
|
834
|
+
if (await this.agreementIsTerminal(agreementId)) {
|
|
827
835
|
return { reason: 'agreement_terminal', because: 'its agreement is terminal' };
|
|
828
836
|
}
|
|
829
837
|
if (this.isSettledTaskEcho(event)) {
|
|
@@ -864,7 +872,7 @@ export class Agent {
|
|
|
864
872
|
*
|
|
865
873
|
* A task frame that carries a terminal state AND a result is an echo of work
|
|
866
874
|
* this agent has already finished: there is no decision left in it. Dropping it
|
|
867
|
-
* is not the same suppression as {@link
|
|
875
|
+
* is not the same suppression as {@link agreementIsTerminal}, which waits
|
|
868
876
|
* for the agreement to end — this fires while the engagement is still live,
|
|
869
877
|
* which is where the cost actually was.
|
|
870
878
|
*
|
|
@@ -1033,17 +1041,74 @@ export class Agent {
|
|
|
1033
1041
|
const tools = this.options.tools ?? this.options.services?.tools ?? [];
|
|
1034
1042
|
return tools.some((t) => toolName(t) === 'connection_request');
|
|
1035
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* The work destination comes from the selected execution first, then the
|
|
1046
|
+
* structured wake. It does not depend on an earlier message being delivered
|
|
1047
|
+
* to this host, so task-first and restarted/scheduled runs work identically.
|
|
1048
|
+
* A reply defaults to the message that woke it, and to the wake's task.
|
|
1049
|
+
* A lane name is never an address: a conversation comes from a message.
|
|
1050
|
+
*/
|
|
1051
|
+
wakeDestinationFor(session, frame, marks) {
|
|
1052
|
+
const asFrame = (value) => value && typeof value === 'object' ? value : {};
|
|
1053
|
+
const text = (value) => typeof value === 'string' && value ? value : undefined;
|
|
1054
|
+
const execution = session.executionContext;
|
|
1055
|
+
const wake = asFrame(frame);
|
|
1056
|
+
const events = wake.type === 'batch' && Array.isArray(wake.events) ? wake.events.map(asFrame) : undefined;
|
|
1057
|
+
// The shared conversations this wake's own messages arrived in.
|
|
1058
|
+
const rooms = [...new Set((events ?? [wake]).filter(e => e.type === 'message').map(e => text(e.addressId)).filter((id) => !!id))];
|
|
1059
|
+
const room = rooms.length === 1 ? rooms[0] : undefined;
|
|
1060
|
+
// A direct execution exists to answer one exact message.
|
|
1061
|
+
const actionMessageId = execution && !execution.agreementId && execution.action?.kind === 'message' ? text(execution.action.id) : undefined;
|
|
1062
|
+
// Agreement work answers in its own conversation; only a lane without work
|
|
1063
|
+
// execution, or a direct execution, keeps the room its messages came from.
|
|
1064
|
+
const remembers = !execution || !!actionMessageId;
|
|
1065
|
+
if (remembers && rooms.length)
|
|
1066
|
+
session.addressId = room && (session.addressId === undefined || session.addressId === room) ? room : null;
|
|
1067
|
+
const remembered = remembers ? session.addressId ?? undefined : undefined;
|
|
1068
|
+
const explicitAgreement = text(wake.agreementId) ?? text(wake.resource?.agreementId) ?? text(wake.result?.agreementId)
|
|
1069
|
+
?? (events ? marks.scope.agreementId : undefined);
|
|
1070
|
+
const explicitChat = text(wake.addressId) ?? text(wake.resource?.addressId);
|
|
1071
|
+
// An exact incoming message supplies its reply address, and a batch of
|
|
1072
|
+
// messages from one room keeps that room; from several rooms the agent
|
|
1073
|
+
// names one. Work execution supplies authority, never a transport
|
|
1074
|
+
// default: under it, other events' navigation hints select no chat.
|
|
1075
|
+
const addressId = wake.type === 'message' ? explicitChat
|
|
1076
|
+
: events ? room ?? (rooms.length ? undefined : remembered)
|
|
1077
|
+
: execution ? remembered
|
|
1078
|
+
: explicitChat ?? remembered;
|
|
1079
|
+
// An unscoped batch cannot borrow a previous customer's agreement.
|
|
1080
|
+
const agreementId = execution ? execution.agreementId ?? undefined
|
|
1081
|
+
: explicitAgreement ?? (events ? undefined : session.roomlessAgreementId);
|
|
1082
|
+
const message = wake.type === 'message' && text(wake.messageId)
|
|
1083
|
+
? { messageId: wake.messageId, ...(text(wake.senderId) ? { senderId: wake.senderId } : {}), ...(text(wake.taskId) ? { taskId: wake.taskId } : {}) }
|
|
1084
|
+
: actionMessageId ? { messageId: actionMessageId } : undefined;
|
|
1085
|
+
const inputRooms = rooms.length > 1 ? rooms : undefined;
|
|
1086
|
+
if (!agreementId && !addressId && !message && !inputRooms)
|
|
1087
|
+
return undefined;
|
|
1088
|
+
const taskId = execution ? execution.taskId ?? undefined : marks.scope.taskId;
|
|
1089
|
+
return {
|
|
1090
|
+
sessionId: session.sessionId,
|
|
1091
|
+
agreementId,
|
|
1092
|
+
addressId,
|
|
1093
|
+
...(message ? { message } : {}),
|
|
1094
|
+
...(taskId ? { taskId } : {}),
|
|
1095
|
+
...(inputRooms ? { inputRooms } : {}),
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1036
1098
|
async _processEvent(event, laneKey) {
|
|
1037
|
-
|
|
1099
|
+
const context = this.sessions.get(laneKey)?.executionContext;
|
|
1100
|
+
if (!context)
|
|
1101
|
+
return this._processEventInContext(event, laneKey);
|
|
1102
|
+
return runWorkExecution(context, this.options.operatorKey, () => this._processEventInContext(event, laneKey), resolveBackendUrl());
|
|
1038
1103
|
}
|
|
1039
1104
|
async _processEventInContext(event, laneKey) {
|
|
1040
1105
|
const session = this.sessions.get(laneKey);
|
|
1041
1106
|
if (!session?.sessionId)
|
|
1042
1107
|
return;
|
|
1043
1108
|
if (session.executionContext)
|
|
1044
|
-
await validateWorkExecution(session.executionContext, this.options.operatorKey, resolveBackendUrl());
|
|
1109
|
+
await validateWorkExecution(currentWorkExecution() ?? session.executionContext, this.options.operatorKey, resolveBackendUrl());
|
|
1045
1110
|
// Presentation may call our sender an rpb-* room binding or the person we
|
|
1046
|
-
// stand in for. Only an exact
|
|
1111
|
+
// stand in for. Only an exact canonical messageId we successfully sent proves
|
|
1047
1112
|
// an echo; another message from the same binding still gets a full wake.
|
|
1048
1113
|
const incoming = event;
|
|
1049
1114
|
const events = incoming?.type === 'batch' && Array.isArray(incoming.events) ? incoming.events : [event];
|
|
@@ -1054,7 +1119,7 @@ export class Agent {
|
|
|
1054
1119
|
&& e?.source === 'active-task-snapshot' && (task?.state === 'active' || task?.state === 'in-progress')
|
|
1055
1120
|
&& task?.assigneeId === this.options.agentId && task.taskId && task.updatedAt && this.confirmedPlanVersions.has(JSON.stringify([laneKey, JSON.stringify([task.taskId, task.updatedAt])])))
|
|
1056
1121
|
return false;
|
|
1057
|
-
return e?.type !== 'message' || !e.messageId || !this.outboundMessages.has(
|
|
1122
|
+
return e?.type !== 'message' || !e.messageId || !this.outboundMessages.has(e.messageId);
|
|
1058
1123
|
});
|
|
1059
1124
|
if (remaining.length === 0) {
|
|
1060
1125
|
this.recordWakeOutcome(session, { kind: 'no-op', reason: events.some(e => e?.type === 'task_result') ? 'own_plan_snapshot' : 'own_message_echo' });
|
|
@@ -1179,7 +1244,9 @@ export class Agent {
|
|
|
1179
1244
|
// a zombie — burning tokens and racing the redelivered retry of the same
|
|
1180
1245
|
// event (duplicate sends, runaway per-wake input).
|
|
1181
1246
|
const tickAbort = new AbortController();
|
|
1182
|
-
const marks =
|
|
1247
|
+
const marks = session.executionContext
|
|
1248
|
+
? new WakeEffects({ agreementId: session.executionContext.agreementId ?? undefined, taskId: session.executionContext.taskId ?? undefined }, false)
|
|
1249
|
+
: WakeEffects.forWake(frame, session.sessionId);
|
|
1183
1250
|
const executionController = this.executionController(identity, tickAbort.signal, taskId => registerTaskRun(taskId));
|
|
1184
1251
|
let heldTask;
|
|
1185
1252
|
const pendingEffects = new Set();
|
|
@@ -1205,6 +1272,8 @@ export class Agent {
|
|
|
1205
1272
|
taskRuns.clear();
|
|
1206
1273
|
};
|
|
1207
1274
|
const checkpoint = async (namedTaskId) => {
|
|
1275
|
+
if (session.executionContext)
|
|
1276
|
+
await validateWorkExecution(currentWorkExecution() ?? session.executionContext, this.options.operatorKey, resolveBackendUrl());
|
|
1208
1277
|
if (tickAbort.signal.aborted) {
|
|
1209
1278
|
if (tickAbort.signal.reason?.code === 'TASK_CONTROL')
|
|
1210
1279
|
heldTask = tickAbort.signal.reason;
|
|
@@ -1263,11 +1332,9 @@ export class Agent {
|
|
|
1263
1332
|
const sameRoom = 'sessionId' in effect && effect.sessionId === session.sessionId;
|
|
1264
1333
|
const scopedEffect = sameRoom && effect.kind === 'read-context'
|
|
1265
1334
|
? { ...effect, opts: { ...effect.opts, workScopes: contextScopesFromWake(frame, session.sessionId) } }
|
|
1266
|
-
: agreementId && effect.kind === '
|
|
1267
|
-
? { ...effect,
|
|
1268
|
-
:
|
|
1269
|
-
? { ...effect, serviceAgreementId: effect.serviceAgreementId ?? agreementId }
|
|
1270
|
-
: effect;
|
|
1335
|
+
: agreementId && (effect.kind === 'tool-call' || effect.kind === 'record-event')
|
|
1336
|
+
? { ...effect, serviceAgreementId: effect.serviceAgreementId ?? agreementId }
|
|
1337
|
+
: effect;
|
|
1271
1338
|
const operation = this.eff(scopedEffect);
|
|
1272
1339
|
pendingEffects.add(operation);
|
|
1273
1340
|
const response = await operation.finally(() => { pendingEffects.delete(operation); });
|
|
@@ -1278,7 +1345,8 @@ export class Agent {
|
|
|
1278
1345
|
};
|
|
1279
1346
|
try {
|
|
1280
1347
|
await checkpoint();
|
|
1281
|
-
const
|
|
1348
|
+
const roomless = this.wakeDestinationFor(session, frame, marks);
|
|
1349
|
+
const tick = () => this.wakeDestination.run(roomless, () => 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, ...(checkout ? { checkout: checkout.session } : {}) })));
|
|
1282
1350
|
tickWork = executionController ? executionControllers.run(executionController, tick) : tick();
|
|
1283
1351
|
const result = await Promise.race([
|
|
1284
1352
|
tickWork,
|
|
@@ -1289,6 +1357,9 @@ export class Agent {
|
|
|
1289
1357
|
}, checkout ? Math.max(this.tickTimeoutMs, CHECKOUT_TICK_TIMEOUT_MS) : this.tickTimeoutMs);
|
|
1290
1358
|
}),
|
|
1291
1359
|
]);
|
|
1360
|
+
// Protected writes validate at their owner. A successful turn may have
|
|
1361
|
+
// completed its own task/answer, so post-turn admission would reject success.
|
|
1362
|
+
// Retained state is validated again before the next turn.
|
|
1292
1363
|
session.ctx = result.ctx;
|
|
1293
1364
|
this.carryExecutionState(session.ctx, executionController);
|
|
1294
1365
|
session.state = result.state;
|
|
@@ -1397,8 +1468,8 @@ export class Agent {
|
|
|
1397
1468
|
const ev = event;
|
|
1398
1469
|
const senderType = String(ev?.senderType ?? '').toUpperCase();
|
|
1399
1470
|
const senderId = ev?.senderId;
|
|
1400
|
-
if (senderId && senderType === 'USER') {
|
|
1401
|
-
this.eff
|
|
1471
|
+
if (senderId && senderType === 'USER' && err?.code !== 'REQUIRED_CONTEXT_UNAVAILABLE') {
|
|
1472
|
+
responseCommand(this.eff, session.sessionId, 'Something went wrong. Please try again.', { recipientId: senderId }).then(command => this.eff({ kind: 'send-message', sessionId: session.sessionId, command })).catch(() => { });
|
|
1402
1473
|
}
|
|
1403
1474
|
// and then say so. Everything above is recovery for the lane —
|
|
1404
1475
|
// it does not mean the event was handled, and the caller is about to
|
|
@@ -1495,13 +1566,15 @@ export class Agent {
|
|
|
1495
1566
|
* Borrowing that path would make a dev tool able to disturb a live one.
|
|
1496
1567
|
*/
|
|
1497
1568
|
async replayWake(record) {
|
|
1498
|
-
|
|
1569
|
+
if (!record.executionContext)
|
|
1570
|
+
return this.replayWakeInContext(record);
|
|
1571
|
+
return runWorkExecution(record.executionContext, this.options.operatorKey, () => this.replayWakeInContext(record), resolveBackendUrl());
|
|
1499
1572
|
}
|
|
1500
1573
|
async replayWakeInContext(record) {
|
|
1501
1574
|
if (!record?.sessionId)
|
|
1502
1575
|
throw new Error('replayWake: record.sessionId is required');
|
|
1503
1576
|
if (record.executionContext)
|
|
1504
|
-
await validateWorkExecution(record.executionContext, this.options.operatorKey, resolveBackendUrl());
|
|
1577
|
+
await validateWorkExecution(currentWorkExecution() ?? record.executionContext, this.options.operatorKey, resolveBackendUrl());
|
|
1505
1578
|
const identity = {
|
|
1506
1579
|
agentId: record.agentId || this.options.agentId,
|
|
1507
1580
|
sessionId: record.sessionId,
|
|
@@ -1522,11 +1595,14 @@ export class Agent {
|
|
|
1522
1595
|
// Same watchdog as a live wake, for the same reason: an unaborted Claude
|
|
1523
1596
|
// query outlives the promise that abandoned it and keeps spending.
|
|
1524
1597
|
const abort = new AbortController();
|
|
1525
|
-
const marks =
|
|
1598
|
+
const marks = record.executionContext
|
|
1599
|
+
? new WakeEffects({ agreementId: record.executionContext.agreementId ?? undefined, taskId: record.executionContext.taskId ?? undefined }, false)
|
|
1600
|
+
: WakeEffects.forWake(record.event, identity.sessionId);
|
|
1601
|
+
const destination = this.wakeDestinationFor({ sessionId: record.sessionId, ownAgentId: identity.agentId, executionContext: record.executionContext }, record.event, marks);
|
|
1526
1602
|
const controller = this.executionController(identity, abort.signal);
|
|
1527
1603
|
let watchdog;
|
|
1528
1604
|
try {
|
|
1529
|
-
const tick = () => this.wakeEffects.run(marks, () => this.brain.tick({
|
|
1605
|
+
const tick = () => this.wakeDestination.run(destination, () => this.wakeEffects.run(marks, () => this.brain.tick({
|
|
1530
1606
|
event: record.event,
|
|
1531
1607
|
ctx: record.ctx,
|
|
1532
1608
|
state: record.state,
|
|
@@ -1535,7 +1611,7 @@ export class Agent {
|
|
|
1535
1611
|
memory,
|
|
1536
1612
|
transitionTap: this.options.transitionTap,
|
|
1537
1613
|
signal: abort.signal,
|
|
1538
|
-
}));
|
|
1614
|
+
})));
|
|
1539
1615
|
const result = await Promise.race([
|
|
1540
1616
|
controller ? executionControllers.run(controller, tick) : tick(),
|
|
1541
1617
|
new Promise((_, reject) => {
|
|
@@ -1612,12 +1688,13 @@ export class Agent {
|
|
|
1612
1688
|
onResourceEvent: (event) => {
|
|
1613
1689
|
return this.handleResourceEvent(event);
|
|
1614
1690
|
},
|
|
1691
|
+
onTaskControlCheck: (taskId) => this.observeBlockedTaskControl(taskId),
|
|
1615
1692
|
// work delivered by the inbox's task channel, not by a chat
|
|
1616
1693
|
// row — this is what reaches a task under a chat-less agreement or one
|
|
1617
1694
|
// the agent assigned to itself.
|
|
1618
|
-
onTask: async (task) => {
|
|
1695
|
+
onTask: async (task, inputs) => {
|
|
1619
1696
|
try {
|
|
1620
|
-
await this.wakeTaskAssignment(task.taskId, { fromInbox: true });
|
|
1697
|
+
await this.wakeTaskAssignment(task.taskId, { fromInbox: true, inputs });
|
|
1621
1698
|
}
|
|
1622
1699
|
catch (err) {
|
|
1623
1700
|
runtimeLog.warn(this.options.name || 'agent', `inbox task ${task.taskId} could not be woken: ${err.message}`);
|
|
@@ -1645,13 +1722,18 @@ export class Agent {
|
|
|
1645
1722
|
const laneKey = laneKeyFor(this.options.agentId, sessionId);
|
|
1646
1723
|
const scope = this.wakeEffects.getStore()?.scope;
|
|
1647
1724
|
const armed = this.stateTimers.armWakeAt(laneKey, at, reason, scope);
|
|
1725
|
+
// The conversation the lane's messages came from, so a restarted host
|
|
1726
|
+
// answers there too instead of reading the lane name as an address.
|
|
1727
|
+
const addressId = this.sessions.get(laneKey)?.addressId;
|
|
1648
1728
|
this.wakeLedger.arm({
|
|
1649
1729
|
laneKey,
|
|
1650
1730
|
sessionId,
|
|
1651
1731
|
firesAt: armed.firesAt,
|
|
1732
|
+
wakeId: armed.wakeId,
|
|
1652
1733
|
reason,
|
|
1653
1734
|
...(scope ? { scope } : {}),
|
|
1654
|
-
|
|
1735
|
+
...(addressId ? { addressId } : {}),
|
|
1736
|
+
executionContext: currentWorkExecution() ? { ...currentWorkExecution(), attemptId: undefined } : undefined,
|
|
1655
1737
|
});
|
|
1656
1738
|
return { ...armed, durable: this.wakeLedger.durable };
|
|
1657
1739
|
}
|
|
@@ -1679,10 +1761,10 @@ export class Agent {
|
|
|
1679
1761
|
if (this.stateTimers.peek(wake.laneKey))
|
|
1680
1762
|
continue;
|
|
1681
1763
|
if (!this.sessions.has(wake.laneKey)) {
|
|
1682
|
-
this.sessions.set(wake.laneKey, { sessionId: wake.sessionId, ownAgentId: agentId, executionContext: wake.executionContext });
|
|
1764
|
+
this.sessions.set(wake.laneKey, { sessionId: wake.sessionId, ownAgentId: agentId, executionContext: wake.executionContext, ...(wake.addressId ? { addressId: wake.addressId } : {}) });
|
|
1683
1765
|
}
|
|
1684
1766
|
try {
|
|
1685
|
-
this.stateTimers.armWakeAt(wake.laneKey, wake.firesAt, wake.reason, wake.scope);
|
|
1767
|
+
this.stateTimers.armWakeAt(wake.laneKey, wake.firesAt, wake.reason, wake.scope, wake.wakeId);
|
|
1686
1768
|
runtimeLog.info(this.options.name || 'agent', `re-armed a saved wake laneKey=${wake.laneKey} at=${new Date(wake.firesAt).toISOString()}`);
|
|
1687
1769
|
}
|
|
1688
1770
|
catch (err) {
|
|
@@ -1705,11 +1787,17 @@ export class Agent {
|
|
|
1705
1787
|
}
|
|
1706
1788
|
const needle = request.match.trim();
|
|
1707
1789
|
const title = request.title?.trim() || needle;
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
agreementId: request.agreementId
|
|
1712
|
-
}
|
|
1790
|
+
// The request is read by its agreement. It has no message and no
|
|
1791
|
+
// conversation, so the wake names neither.
|
|
1792
|
+
try {
|
|
1793
|
+
await this.handleMessage(`New marketplace request matching your tags (${needle}): ${title}. Claim with agreement_claim if you will take it.`, { agreementId: request.agreementId });
|
|
1794
|
+
}
|
|
1795
|
+
catch (err) {
|
|
1796
|
+
// An open request is an offer to look, not assigned work, and it stays
|
|
1797
|
+
// open in the marketplace. A wake that could not read it must not hold
|
|
1798
|
+
// every other delivery in the pass behind it.
|
|
1799
|
+
runtimeLog.warn(this.options.name || 'agent', `request ${request.agreementId}: wake failed, left open in the marketplace: ${err?.message || err}`);
|
|
1800
|
+
}
|
|
1713
1801
|
}
|
|
1714
1802
|
/** replay task-assigned for an existing active task (demo recovery). */
|
|
1715
1803
|
async wakeTaskAssignment(taskId, options) {
|
|
@@ -1722,21 +1810,23 @@ export class Agent {
|
|
|
1722
1810
|
if (state !== 'active' && state !== 'in-progress') {
|
|
1723
1811
|
return;
|
|
1724
1812
|
}
|
|
1813
|
+
const selected = currentWorkExecution();
|
|
1725
1814
|
const sessionId = laneSessionIdForTask(full);
|
|
1726
|
-
const laneKey = laneKeyFor(agentId, sessionId);
|
|
1815
|
+
const laneKey = laneKeyFor(agentId, sessionId, selected?.contextId);
|
|
1727
1816
|
if (!this.sessions.has(laneKey)) {
|
|
1728
1817
|
this.sessions.set(laneKey, { sessionId, ownAgentId: agentId, executionContext: currentWorkExecution() });
|
|
1729
1818
|
}
|
|
1730
1819
|
await this.eventQueue.enqueue({
|
|
1731
1820
|
type: 'task_result',
|
|
1732
|
-
|
|
1821
|
+
// New facts must not disappear behind an unchanged task snapshot version.
|
|
1822
|
+
...(options?.fromInbox && !options.inputs?.length ? { source: 'active-task-snapshot' } : {}),
|
|
1733
1823
|
result: {
|
|
1734
1824
|
...full,
|
|
1825
|
+
...(options?.inputs?.length ? { inputs: options.inputs } : {}),
|
|
1735
1826
|
taskId: full.taskId ?? taskId,
|
|
1736
1827
|
state,
|
|
1737
1828
|
providerIsYou: true,
|
|
1738
1829
|
},
|
|
1739
|
-
chatId: sessionId,
|
|
1740
1830
|
}, laneKey);
|
|
1741
1831
|
}
|
|
1742
1832
|
/**
|
|
@@ -5,7 +5,6 @@ import type { ToolManager } from '../tools/ToolManager.js';
|
|
|
5
5
|
import { type RawEvent } from '../runtime/buildOutcome.js';
|
|
6
6
|
import type { TickInput, TickOutput } from './ZiggsBrain.js';
|
|
7
7
|
import { type AskWakeGate } from './wakeGate.js';
|
|
8
|
-
export { isPublishProbeChat } from '../probeChat.js';
|
|
9
8
|
/**
|
|
10
9
|
* status notices must not spend a Claude turn answering
|
|
11
10
|
* nobody. Real work wakes arrive as chat `message`, synthetic `task_result`
|