@ziggs-ai/agent-sdk 0.41.2 → 0.42.2
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 +2 -0
- package/dist/Agent.d.ts +10 -0
- package/dist/Agent.js +46 -11
- package/dist/brain/claudeSdkTools.d.ts +2 -2
- package/dist/brain/claudeSdkTools.js +3 -2
- package/dist/brain/wakeGate.js +2 -2
- package/dist/checkout/boxClient.d.ts +1 -0
- package/dist/checkout/boxClient.js +2 -3
- package/dist/checkout/open.js +2 -0
- package/dist/mcp/createMcpAgent.js +40 -8
- package/dist/platform/WakeLedger.d.ts +2 -0
- package/dist/platform/WakeLedger.js +1 -0
- package/dist/platform/ZiggsEffectHandler.js +1 -1
- package/dist/platform/agreements/AgreementService.d.ts +3 -1
- package/dist/platform/agreements/AgreementService.js +2 -2
- package/dist/platform/inboxLoop.d.ts +9 -0
- package/dist/platform/inboxLoop.js +78 -27
- package/dist/platform/laneKey.d.ts +1 -1
- package/dist/platform/laneKey.js +3 -2
- package/dist/platform/tools/index.d.ts +1 -1
- package/dist/platform/tools/index.js +1 -1
- package/dist/platform/tools/tier1/protocolRunner.js +4 -2
- package/dist/platform/tools/tier1/protocolTools.js +5 -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/package.json +3 -3
package/CHANGELOG.md
CHANGED
package/dist/Agent.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type WorkExecutionContext } from '@ziggs-ai/api-client';
|
|
1
2
|
import type { PollBinding, PollKind } from './platform/polls/PollKind.js';
|
|
2
3
|
export { workScopeFromWake } from './runtime/wakeEffects.js';
|
|
3
4
|
import type { AgentConfig, Ctx, Workflow } from './types.js';
|
|
@@ -212,6 +213,7 @@ export interface WakeRecord {
|
|
|
212
213
|
agentId: string;
|
|
213
214
|
sessionId: string;
|
|
214
215
|
laneKey: string;
|
|
216
|
+
executionContext?: WorkExecutionContext;
|
|
215
217
|
/** The event frame that woke the agent. */
|
|
216
218
|
event: unknown;
|
|
217
219
|
/** FSM context carried into the tick — undefined on a cold lane. */
|
|
@@ -298,6 +300,12 @@ export declare class Agent {
|
|
|
298
300
|
private carryExecutionState;
|
|
299
301
|
/** Runtime control is a task fact, never model-interpreted message text. */
|
|
300
302
|
private taskExecutionControl;
|
|
303
|
+
/**
|
|
304
|
+
* Control on an assignment the inbox will not run yet. No wake follows, so a
|
|
305
|
+
* resume is acknowledged here too once no run of the task is live; the wake
|
|
306
|
+
* path that would otherwise answer it never comes while the delivery is blocked.
|
|
307
|
+
*/
|
|
308
|
+
private observeBlockedTaskControl;
|
|
301
309
|
private acknowledgeExecutionControl;
|
|
302
310
|
/**
|
|
303
311
|
* Handler for a resource change (artifact created, task state transition,
|
|
@@ -411,6 +419,7 @@ export declare class Agent {
|
|
|
411
419
|
*/
|
|
412
420
|
private canAskForAccess;
|
|
413
421
|
private _processEvent;
|
|
422
|
+
private _processEventInContext;
|
|
414
423
|
/**
|
|
415
424
|
* The checkout a wake needs, opened before the brain runs. Null when this
|
|
416
425
|
* wake needs none; `'refused'` when the task was failed here by name.
|
|
@@ -433,6 +442,7 @@ export declare class Agent {
|
|
|
433
442
|
* Borrowing that path would make a dev tool able to disturb a live one.
|
|
434
443
|
*/
|
|
435
444
|
replayWake(record: WakeRecord): Promise<TickOutput>;
|
|
445
|
+
private replayWakeInContext;
|
|
436
446
|
/** Arms the lane's liveness timer iff stateName is a parked state declaring timeoutMs. */
|
|
437
447
|
private _armIfTimedParked;
|
|
438
448
|
private _resolveOwnAgentId;
|
package/dist/Agent.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateWorkExecution, withWorkExecution, currentWorkExecution } from '@ziggs-ai/api-client';
|
|
1
2
|
import { SERVER_EVENTS } from '@ziggs-ai/contracts';
|
|
2
3
|
import { withAgreementContext } from './runtime/agreementContext.js';
|
|
3
4
|
import { agreementLaneId, agreementIdFromLane, isContextAccessRequest } from '@ziggs-ai/api-client';
|
|
@@ -521,8 +522,10 @@ export class Agent {
|
|
|
521
522
|
if (!this.taskService)
|
|
522
523
|
return null;
|
|
523
524
|
let task;
|
|
525
|
+
// Host control observation is separate from task work. Only execution state
|
|
526
|
+
// leaves this method; a stopped work context cannot observe its own stop.
|
|
524
527
|
try {
|
|
525
|
-
task = await this.taskService.getTask(taskId);
|
|
528
|
+
task = await withWorkExecution(undefined, () => this.taskService.getTask(taskId));
|
|
526
529
|
}
|
|
527
530
|
catch (error) {
|
|
528
531
|
// Unreadable is not a hold. The wake filter still drops a frame whose
|
|
@@ -536,6 +539,17 @@ export class Agent {
|
|
|
536
539
|
return null;
|
|
537
540
|
return { blocked: task.state === 'cancelled' || (task.state === 'active' && task.control?.action === 'pause'), closed: task.state === 'cancelled', control: task.control };
|
|
538
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* Control on an assignment the inbox will not run yet. No wake follows, so a
|
|
544
|
+
* resume is acknowledged here too once no run of the task is live; the wake
|
|
545
|
+
* path that would otherwise answer it never comes while the delivery is blocked.
|
|
546
|
+
*/
|
|
547
|
+
async observeBlockedTaskControl(taskId) {
|
|
548
|
+
const execution = await this.taskExecutionControl(taskId);
|
|
549
|
+
const idleResume = execution?.control?.action === 'resume' && !this.activeTaskRuns.get(taskId)?.size;
|
|
550
|
+
if (execution?.blocked || idleResume)
|
|
551
|
+
await this.acknowledgeExecutionControl(taskId, execution?.control);
|
|
552
|
+
}
|
|
539
553
|
async acknowledgeExecutionControl(taskId, control) {
|
|
540
554
|
if (control?.status !== 'requested')
|
|
541
555
|
return;
|
|
@@ -626,9 +640,10 @@ export class Agent {
|
|
|
626
640
|
}
|
|
627
641
|
if (!sessionId)
|
|
628
642
|
return;
|
|
629
|
-
const
|
|
643
|
+
const executionContext = event.executionContext;
|
|
644
|
+
const laneKey = laneKeyFor(ownAgentId, sessionId, executionContext?.contextId);
|
|
630
645
|
if (!this.sessions.has(laneKey)) {
|
|
631
|
-
this.sessions.set(laneKey, { sessionId, ownAgentId });
|
|
646
|
+
this.sessions.set(laneKey, { sessionId, ownAgentId, executionContext });
|
|
632
647
|
}
|
|
633
648
|
const agreementId = resumeAgreementId ||
|
|
634
649
|
(typeof event.agreementId === 'string' && event.agreementId) ||
|
|
@@ -656,7 +671,7 @@ export class Agent {
|
|
|
656
671
|
['proposal_approved', 'proposal_rejected'].includes(notice?.operation ?? '') &&
|
|
657
672
|
isContextAccessRequest(notice?.agreement?.grantRequest))
|
|
658
673
|
return;
|
|
659
|
-
const messageId = metadata.messageId;
|
|
674
|
+
const messageId = (metadata.responsibilityId ?? metadata.messageId);
|
|
660
675
|
if (messageId && this.seenMessages.has(messageId)) {
|
|
661
676
|
runtimeLog.debug('Agent', `skip duplicate messageId=${messageId}`);
|
|
662
677
|
return;
|
|
@@ -688,9 +703,10 @@ export class Agent {
|
|
|
688
703
|
runtimeLog.warn('Agent', `session resolver returned null ownAgentId=${ownAgentId} chatId=${normalized.chatId ?? 'null'}`);
|
|
689
704
|
return;
|
|
690
705
|
}
|
|
691
|
-
const
|
|
706
|
+
const executionContext = metadata.executionContext;
|
|
707
|
+
const laneKey = laneKeyFor(ownAgentId, sessionId, executionContext?.contextId);
|
|
692
708
|
if (!this.sessions.has(laneKey)) {
|
|
693
|
-
this.sessions.set(laneKey, { sessionId, ownAgentId });
|
|
709
|
+
this.sessions.set(laneKey, { sessionId, ownAgentId, executionContext });
|
|
694
710
|
}
|
|
695
711
|
// The inbox loop owns ack (act-before-ack, delivery law); the agent only
|
|
696
712
|
// routes the event onto its lane. No live-path watermark ack — there is no
|
|
@@ -745,6 +761,9 @@ export class Agent {
|
|
|
745
761
|
}), isTelemetryIngestEnabled(this.options.telemetryIngest));
|
|
746
762
|
}
|
|
747
763
|
memoryForWake(session, event) {
|
|
764
|
+
const execution = currentWorkExecution();
|
|
765
|
+
if (execution)
|
|
766
|
+
return partitionMemoryStore(this.memory, `execution:${execution.contextId}`);
|
|
748
767
|
const fromEvent = hireIdsFromEvent(event);
|
|
749
768
|
const wakeAgreementIds = fromEvent.length ? fromEvent : uniqueHireIds([session.actingHireId]);
|
|
750
769
|
const section = memorySectionForWake({
|
|
@@ -939,7 +958,9 @@ export class Agent {
|
|
|
939
958
|
// say so in a sentence, and muting it would be a worse silence.
|
|
940
959
|
let assigned;
|
|
941
960
|
try {
|
|
942
|
-
|
|
961
|
+
// A host check across this agent's own plate, not a read inside the
|
|
962
|
+
// work: it runs outside the selected execution.
|
|
963
|
+
assigned = await withWorkExecution(undefined, () => this.taskService.getActiveTasksForAgent(agentId));
|
|
943
964
|
}
|
|
944
965
|
catch (err) {
|
|
945
966
|
runtimeLog.warn(this.options.name || 'agent', `needs check could not read assigned work: ${err?.message ?? err}`);
|
|
@@ -1026,9 +1047,14 @@ export class Agent {
|
|
|
1026
1047
|
return tools.some((t) => toolName(t) === 'connection_request');
|
|
1027
1048
|
}
|
|
1028
1049
|
async _processEvent(event, laneKey) {
|
|
1050
|
+
return withWorkExecution(this.sessions.get(laneKey)?.executionContext, () => this._processEventInContext(event, laneKey));
|
|
1051
|
+
}
|
|
1052
|
+
async _processEventInContext(event, laneKey) {
|
|
1029
1053
|
const session = this.sessions.get(laneKey);
|
|
1030
1054
|
if (!session?.sessionId)
|
|
1031
1055
|
return;
|
|
1056
|
+
if (session.executionContext)
|
|
1057
|
+
await validateWorkExecution(session.executionContext, this.options.operatorKey, resolveBackendUrl());
|
|
1032
1058
|
// Presentation may call our sender an rpb-* room binding or the person we
|
|
1033
1059
|
// stand in for. Only an exact (chat, messageId) we successfully sent proves
|
|
1034
1060
|
// an echo; another message from the same binding still gets a full wake.
|
|
@@ -1144,6 +1170,7 @@ export class Agent {
|
|
|
1144
1170
|
if (this.options.wakeTap) {
|
|
1145
1171
|
try {
|
|
1146
1172
|
this.options.wakeTap({
|
|
1173
|
+
executionContext: session.executionContext,
|
|
1147
1174
|
agentId: identity.agentId,
|
|
1148
1175
|
sessionId: identity.sessionId,
|
|
1149
1176
|
laneKey,
|
|
@@ -1439,7 +1466,7 @@ export class Agent {
|
|
|
1439
1466
|
needs,
|
|
1440
1467
|
taskId,
|
|
1441
1468
|
getTask: (id) => this.taskService.getTask(id),
|
|
1442
|
-
listConnections: () => new ConnectionsClient(operatorKey, agentId, undefined
|
|
1469
|
+
listConnections: () => new ConnectionsClient(operatorKey, agentId, undefined).listForHolder(),
|
|
1443
1470
|
door: { backendUrl: resolveBackendUrl(), operatorKey, agentId, laneId },
|
|
1444
1471
|
failTask: async (id, reason) => {
|
|
1445
1472
|
await this.taskService.updateState(id, 'failed', { summary: reason, status: 'failed' });
|
|
@@ -1481,8 +1508,13 @@ export class Agent {
|
|
|
1481
1508
|
* Borrowing that path would make a dev tool able to disturb a live one.
|
|
1482
1509
|
*/
|
|
1483
1510
|
async replayWake(record) {
|
|
1511
|
+
return withWorkExecution(record.executionContext, () => this.replayWakeInContext(record));
|
|
1512
|
+
}
|
|
1513
|
+
async replayWakeInContext(record) {
|
|
1484
1514
|
if (!record?.sessionId)
|
|
1485
1515
|
throw new Error('replayWake: record.sessionId is required');
|
|
1516
|
+
if (record.executionContext)
|
|
1517
|
+
await validateWorkExecution(record.executionContext, this.options.operatorKey, resolveBackendUrl());
|
|
1486
1518
|
const identity = {
|
|
1487
1519
|
agentId: record.agentId || this.options.agentId,
|
|
1488
1520
|
sessionId: record.sessionId,
|
|
@@ -1593,6 +1625,7 @@ export class Agent {
|
|
|
1593
1625
|
onResourceEvent: (event) => {
|
|
1594
1626
|
return this.handleResourceEvent(event);
|
|
1595
1627
|
},
|
|
1628
|
+
onTaskControlCheck: (taskId) => this.observeBlockedTaskControl(taskId),
|
|
1596
1629
|
// work delivered by the inbox's task channel, not by a chat
|
|
1597
1630
|
// row — this is what reaches a task under a chat-less agreement or one
|
|
1598
1631
|
// the agent assigned to itself.
|
|
@@ -1602,6 +1635,7 @@ export class Agent {
|
|
|
1602
1635
|
}
|
|
1603
1636
|
catch (err) {
|
|
1604
1637
|
runtimeLog.warn(this.options.name || 'agent', `inbox task ${task.taskId} could not be woken: ${err.message}`);
|
|
1638
|
+
throw err;
|
|
1605
1639
|
}
|
|
1606
1640
|
},
|
|
1607
1641
|
// exact-match triage in code — miss acks with 0 tokens.
|
|
@@ -1631,6 +1665,7 @@ export class Agent {
|
|
|
1631
1665
|
firesAt: armed.firesAt,
|
|
1632
1666
|
reason,
|
|
1633
1667
|
...(scope ? { scope } : {}),
|
|
1668
|
+
executionContext: currentWorkExecution(),
|
|
1634
1669
|
});
|
|
1635
1670
|
return { ...armed, durable: this.wakeLedger.durable };
|
|
1636
1671
|
}
|
|
@@ -1653,12 +1688,12 @@ export class Agent {
|
|
|
1653
1688
|
return;
|
|
1654
1689
|
}
|
|
1655
1690
|
for (const wake of pending) {
|
|
1656
|
-
if (wake.laneKey !== laneKeyFor(agentId, wake.sessionId))
|
|
1691
|
+
if (wake.laneKey !== laneKeyFor(agentId, wake.sessionId, wake.executionContext?.contextId))
|
|
1657
1692
|
continue;
|
|
1658
1693
|
if (this.stateTimers.peek(wake.laneKey))
|
|
1659
1694
|
continue;
|
|
1660
1695
|
if (!this.sessions.has(wake.laneKey)) {
|
|
1661
|
-
this.sessions.set(wake.laneKey, { sessionId: wake.sessionId, ownAgentId: agentId });
|
|
1696
|
+
this.sessions.set(wake.laneKey, { sessionId: wake.sessionId, ownAgentId: agentId, executionContext: wake.executionContext });
|
|
1662
1697
|
}
|
|
1663
1698
|
try {
|
|
1664
1699
|
this.stateTimers.armWakeAt(wake.laneKey, wake.firesAt, wake.reason, wake.scope);
|
|
@@ -1704,7 +1739,7 @@ export class Agent {
|
|
|
1704
1739
|
const sessionId = laneSessionIdForTask(full);
|
|
1705
1740
|
const laneKey = laneKeyFor(agentId, sessionId);
|
|
1706
1741
|
if (!this.sessions.has(laneKey)) {
|
|
1707
|
-
this.sessions.set(laneKey, { sessionId, ownAgentId: agentId });
|
|
1742
|
+
this.sessions.set(laneKey, { sessionId, ownAgentId: agentId, executionContext: currentWorkExecution() });
|
|
1708
1743
|
}
|
|
1709
1744
|
await this.eventQueue.enqueue({
|
|
1710
1745
|
type: 'task_result',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ToolDefinition } from '../tools/defineTool.js';
|
|
2
|
-
import { cancelTaskTool, listTasksTool } from '../platform/tools/tier2/taskTools.js';
|
|
2
|
+
import { cancelTaskTool, listTasksTool, delegateTaskTool } from '../platform/tools/tier2/taskTools.js';
|
|
3
3
|
import type { EffectHandler, DeliveryWriteKeys } from '../types.js';
|
|
4
4
|
import type { MemoryStore } from '../memory/MemoryStore.js';
|
|
5
5
|
export interface ClaudeSdkToolContext extends Record<string, unknown> {
|
|
@@ -25,5 +25,5 @@ export interface ClaudeSdkToolContext extends Record<string, unknown> {
|
|
|
25
25
|
}
|
|
26
26
|
export declare const contextSnapshotTool: ToolDefinition;
|
|
27
27
|
export declare const chatSendTool: ToolDefinition;
|
|
28
|
-
export { listTasksTool, cancelTaskTool };
|
|
28
|
+
export { listTasksTool, cancelTaskTool, delegateTaskTool };
|
|
29
29
|
export declare const CLAUDE_SDK_TOOLS: ToolDefinition[];
|
|
@@ -5,7 +5,7 @@ import { credsFromToolContext } from '../tools/toolCreds.js';
|
|
|
5
5
|
import { sendChatMessage, presentSendResult } from '@ziggs-ai/api-client';
|
|
6
6
|
import { defineTool } from '../tools/defineTool.js';
|
|
7
7
|
import { listGrantsTool } from '../platform/tools/tier2/grantsTools.js';
|
|
8
|
-
import { cancelTaskTool, listTasksTool } from '../platform/tools/tier2/taskTools.js';
|
|
8
|
+
import { cancelTaskTool, listTasksTool, delegateTaskTool } from '../platform/tools/tier2/taskTools.js';
|
|
9
9
|
import { CHAT_TOOLS } from '../platform/tools/tier2/chatTools.js';
|
|
10
10
|
// default message-history cap. An uncapped snapshot returns the whole
|
|
11
11
|
// chat — in a delegation chat that accumulates notification rows, every wake's
|
|
@@ -168,13 +168,14 @@ export const chatSendTool = defineTool('chat_send', {
|
|
|
168
168
|
// task_list IS here: MCP delegates have ziggs_task_list; the hosted
|
|
169
169
|
// Claude surface previously had no way to enumerate its own open tasks (reads
|
|
170
170
|
// were prompt-injected only). Same GET /tasks capability as the MCP tool.
|
|
171
|
-
export { listTasksTool, cancelTaskTool };
|
|
171
|
+
export { listTasksTool, cancelTaskTool, delegateTaskTool };
|
|
172
172
|
export const CLAUDE_SDK_TOOLS = [
|
|
173
173
|
contextSnapshotTool,
|
|
174
174
|
chatSendTool,
|
|
175
175
|
listGrantsTool,
|
|
176
176
|
listTasksTool,
|
|
177
177
|
cancelTaskTool,
|
|
178
|
+
delegateTaskTool,
|
|
178
179
|
// store-claim providers must open the buyer chat from the default
|
|
179
180
|
// Claude surface (chat_open was tier-2 / email-agent opt-in only).
|
|
180
181
|
...CHAT_TOOLS,
|
package/dist/brain/wakeGate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildOperatorHeaders, getBackendUrl } from '@ziggs-ai/api-client';
|
|
1
|
+
import { buildOperatorHeaders, getBackendUrl, withoutExecution } from '@ziggs-ai/api-client';
|
|
2
2
|
import { getBatchEvents } from '../context/batch.js';
|
|
3
3
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
4
4
|
/** Longer than Jev's own 4s budget on the server, shorter than a turn is worth waiting for. */
|
|
@@ -52,7 +52,7 @@ export function backendWakeGate(creds) {
|
|
|
52
52
|
const res = await fetch(`${getBackendUrl()}/internal/wake-gate`, {
|
|
53
53
|
method: 'POST',
|
|
54
54
|
headers: {
|
|
55
|
-
...buildOperatorHeaders(creds.operatorKey, creds.agentId),
|
|
55
|
+
...withoutExecution(buildOperatorHeaders(creds.operatorKey, creds.agentId)),
|
|
56
56
|
'Content-Type': 'application/json',
|
|
57
57
|
},
|
|
58
58
|
body: JSON.stringify(req),
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { laneHeader } from '@ziggs-ai/api-client';
|
|
2
1
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
2
|
/** Headers git sends that the door needs to see; anything else stays behind. */
|
|
4
3
|
const GIT_REQUEST_HEADERS = ['content-type', 'content-encoding', 'accept', 'git-protocol'];
|
|
@@ -23,8 +22,8 @@ export function doorHeaders(door, gitHeaders) {
|
|
|
23
22
|
'x-grant-id': door.grantId,
|
|
24
23
|
'x-ziggs-task-id': door.taskId,
|
|
25
24
|
};
|
|
26
|
-
|
|
27
|
-
out[
|
|
25
|
+
if (door.executionContextId)
|
|
26
|
+
out['x-ziggs-execution'] = door.executionContextId;
|
|
28
27
|
for (const [k, v] of Object.entries(gitHeaders)) {
|
|
29
28
|
if (GIT_REQUEST_HEADERS.includes(k.toLowerCase()))
|
|
30
29
|
out[k.toLowerCase()] = v;
|
package/dist/checkout/open.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { assistantRequestHeaders } from '@ziggs-ai/api-client';
|
|
1
2
|
import { createHash, randomBytes } from 'node:crypto';
|
|
2
3
|
import { CheckoutBoxClient, doorHeaders } from './boxClient.js';
|
|
3
4
|
import { checkoutBranchForTask, checkoutTargetFromTask } from './intake.js';
|
|
@@ -69,6 +70,7 @@ export async function openCheckout(deps) {
|
|
|
69
70
|
continue;
|
|
70
71
|
const candidate = {
|
|
71
72
|
...deps.door,
|
|
73
|
+
executionContextId: assistantRequestHeaders()['X-Ziggs-Execution'],
|
|
72
74
|
connectionId: group.connectionId,
|
|
73
75
|
grantId,
|
|
74
76
|
taskId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveWebAppOrigin, connectionsSettingsAppUrl,
|
|
1
|
+
import { resolveWebAppOrigin, connectionsSettingsAppUrl, assistantRequestHeaders, currentWorkExecution } from '@ziggs-ai/api-client';
|
|
2
2
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
3
3
|
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
4
4
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
@@ -24,13 +24,19 @@ export class ZiggsGatewayError extends Error {
|
|
|
24
24
|
this.status = status;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
|
+
/** Open gateway sessions kept per selected execution before the oldest closes. */
|
|
28
|
+
const MAX_WORK_SESSIONS = 16;
|
|
27
29
|
/**
|
|
28
30
|
* Wrap fetch so structured gateway denials (401/403 with a `code` body)
|
|
29
31
|
* surface as ZiggsGatewayError instead of a generic transport failure.
|
|
30
32
|
*/
|
|
31
33
|
export function wrapGatewayFetch(baseFetch = fetch) {
|
|
32
34
|
return (async (input, init) => {
|
|
33
|
-
const
|
|
35
|
+
const headers = new Headers(init?.headers);
|
|
36
|
+
headers.delete('X-Ziggs-Execution');
|
|
37
|
+
for (const [key, value] of Object.entries(assistantRequestHeaders()))
|
|
38
|
+
headers.set(key, value);
|
|
39
|
+
const response = await baseFetch(input, { ...init, headers });
|
|
34
40
|
if (response.status === 401 || response.status === 403) {
|
|
35
41
|
const body = (await response
|
|
36
42
|
.clone()
|
|
@@ -71,7 +77,7 @@ export function resolveZiggsGatewayTarget(config, defaults = {}) {
|
|
|
71
77
|
// its lane, so the same account and the same grant got two different
|
|
72
78
|
// answers depending on which door the agent used. Built by the shared
|
|
73
79
|
// helper, because a copy of this header going missing is invisible.
|
|
74
|
-
...
|
|
80
|
+
...assistantRequestHeaders(),
|
|
75
81
|
},
|
|
76
82
|
};
|
|
77
83
|
}
|
|
@@ -97,7 +103,7 @@ export function resolvePerUserGatewayTarget(config, agentId, userId, onBehalfOfA
|
|
|
97
103
|
// uses this agreement to resolve the delegation root's user.
|
|
98
104
|
...(onBehalfOfAgreementId ? { 'x-on-behalf-of-agreement': onBehalfOfAgreementId } : {}),
|
|
99
105
|
// The lane, for the same reason as the path-id door above.
|
|
100
|
-
...
|
|
106
|
+
...assistantRequestHeaders(),
|
|
101
107
|
},
|
|
102
108
|
};
|
|
103
109
|
}
|
|
@@ -343,7 +349,7 @@ export async function createMcpAgent(options) {
|
|
|
343
349
|
// grants this session may spend, so sessions must not be shared across
|
|
344
350
|
// any of the three. The headers are fixed when the transport is built,
|
|
345
351
|
// so a session reused across lanes would carry the first lane's fence.
|
|
346
|
-
const cacheKey = `${userId}|${onBehalfOfAgreementId ?? ''}|${
|
|
352
|
+
const cacheKey = `${userId}|${onBehalfOfAgreementId ?? ''}|${assistantRequestHeaders()['X-Ziggs-Execution'] ?? ''}`;
|
|
347
353
|
let p = clients.get(cacheKey);
|
|
348
354
|
if (!p) {
|
|
349
355
|
p = (async () => {
|
|
@@ -387,7 +393,7 @@ export async function createMcpAgent(options) {
|
|
|
387
393
|
if (!operatorKey)
|
|
388
394
|
return null;
|
|
389
395
|
try {
|
|
390
|
-
const body = await new ConnectionsClient(operatorKey, agentId, backendBase
|
|
396
|
+
const body = await new ConnectionsClient(operatorKey, agentId, backendBase).requestMcpConnection({
|
|
391
397
|
serverUrl: transportConfig.serverUrl,
|
|
392
398
|
tools: tools.map(t => t.name),
|
|
393
399
|
onBehalfOfUserId: userId,
|
|
@@ -477,7 +483,31 @@ export async function createMcpAgent(options) {
|
|
|
477
483
|
try {
|
|
478
484
|
await client.connect(transport);
|
|
479
485
|
const rawTools = capTools((await client.listTools()).tools, agentOptions.agentId, maxTools, extraTools.length, allowedTools);
|
|
480
|
-
|
|
486
|
+
// A brokered gateway session is bound to the work it was opened in, so a
|
|
487
|
+
// call made inside a selected execution opens (and keeps) a session of its
|
|
488
|
+
// own; the boot session serves calls made outside any execution.
|
|
489
|
+
const workClients = new Map();
|
|
490
|
+
const clientFor = () => {
|
|
491
|
+
const work = transportConfig.type === 'ziggs' ? currentWorkExecution()?.contextId : undefined;
|
|
492
|
+
if (!work)
|
|
493
|
+
return Promise.resolve(client);
|
|
494
|
+
let pending = workClients.get(work);
|
|
495
|
+
if (!pending) {
|
|
496
|
+
if (workClients.size >= MAX_WORK_SESSIONS) {
|
|
497
|
+
const [oldest, closing] = workClients.entries().next().value;
|
|
498
|
+
workClients.delete(oldest);
|
|
499
|
+
void closing.then(c => c.close()).catch(() => { });
|
|
500
|
+
}
|
|
501
|
+
const scoped = new Client({ name: 'ziggs-mcp-agent', version: '1.0.0' });
|
|
502
|
+
pending = scoped
|
|
503
|
+
.connect(buildTransport(transportConfig, { agentId: agentOptions.agentId }))
|
|
504
|
+
.then(() => scoped);
|
|
505
|
+
pending.catch(() => workClients.delete(work));
|
|
506
|
+
workClients.set(work, pending);
|
|
507
|
+
}
|
|
508
|
+
return pending;
|
|
509
|
+
};
|
|
510
|
+
const caller = async (name, args) => (await clientFor()).callTool({ name, arguments: args });
|
|
481
511
|
const mcpTools = mapMcpTools(caller, rawTools);
|
|
482
512
|
const resolvedWorkflow = workflow ?? buildDefaultWorkflow(agentOptions.description ?? '', rawTools);
|
|
483
513
|
const agent = createAgent({
|
|
@@ -487,7 +517,9 @@ export async function createMcpAgent(options) {
|
|
|
487
517
|
});
|
|
488
518
|
return {
|
|
489
519
|
agent,
|
|
490
|
-
closeMcp: () =>
|
|
520
|
+
closeMcp: async () => {
|
|
521
|
+
await Promise.all([client, ...workClients.values()].map(c => Promise.resolve(c).then(open => open.close()).catch(() => { })));
|
|
522
|
+
},
|
|
491
523
|
toolNames: rawTools.map((t) => t.name),
|
|
492
524
|
};
|
|
493
525
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkExecutionContext } from '@ziggs-ai/api-client';
|
|
1
2
|
import { type MemoryStore } from '../memory/MemoryStore.js';
|
|
2
3
|
import type { WorkScope } from '../runtime/wakeEffects.js';
|
|
3
4
|
/** One check-back an agent asked for, as it is kept across a restart. */
|
|
@@ -7,6 +8,7 @@ export interface SavedWake {
|
|
|
7
8
|
firesAt: number;
|
|
8
9
|
reason: string | null;
|
|
9
10
|
scope?: WorkScope;
|
|
11
|
+
executionContext?: WorkExecutionContext;
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Where an agent's own check-backs are kept, so a restart does not lose them.
|
|
@@ -28,7 +28,7 @@ async function buildSnapshot(deps, chatId, agentId, opts = {}) {
|
|
|
28
28
|
// Read as this wake: the session is the lane, the way its tool calls are
|
|
29
29
|
// sent. Without it a room filed in another workspace — a stranger's room
|
|
30
30
|
// with a listed agent — reads as missing and the wake is skipped.
|
|
31
|
-
const reader = deps.contextReadClient
|
|
31
|
+
const reader = deps.contextReadClient;
|
|
32
32
|
const snap = await reader.snapshot(chatId, {
|
|
33
33
|
maxMessages: opts.limit,
|
|
34
34
|
...opts.workScope,
|
|
@@ -29,7 +29,9 @@ export declare class AgreementService {
|
|
|
29
29
|
*/
|
|
30
30
|
publishOffer(payload: PublishOfferPayload): Promise<Agreement>;
|
|
31
31
|
delegate(payload: DelegateAgreementData): Promise<Agreement>;
|
|
32
|
-
respond(agreementId: string, action: 'approve' | 'reject'
|
|
32
|
+
respond(agreementId: string, action: 'approve' | 'reject', opts?: {
|
|
33
|
+
mandateAgreementId?: string;
|
|
34
|
+
}): Promise<Agreement>;
|
|
33
35
|
/**
|
|
34
36
|
* claim any open broadcast (request, standing offer, hand-off,
|
|
35
37
|
* link invite). Routing (offer vs agreement claim endpoint) is decided in
|
|
@@ -44,8 +44,8 @@ export class AgreementService {
|
|
|
44
44
|
delegate(payload) {
|
|
45
45
|
return delegateAgreement(payload, this.creds);
|
|
46
46
|
}
|
|
47
|
-
respond(agreementId, action) {
|
|
48
|
-
return respondToAgreement(agreementId, action, this.creds);
|
|
47
|
+
respond(agreementId, action, opts = {}) {
|
|
48
|
+
return respondToAgreement(agreementId, action, this.creds, opts);
|
|
49
49
|
}
|
|
50
50
|
/**
|
|
51
51
|
* claim any open broadcast (request, standing offer, hand-off,
|
|
@@ -32,6 +32,8 @@ export interface InboxLoopDeps {
|
|
|
32
32
|
* public request burns a full Claude turn per published supplier.
|
|
33
33
|
*/
|
|
34
34
|
onRequest?: (request: InboxRequestRef) => Promise<void>;
|
|
35
|
+
/** Host-only control observation for blocked task assignments; never a model wake. */
|
|
36
|
+
onTaskControlCheck?: (taskId: string) => Promise<void>;
|
|
35
37
|
label?: string;
|
|
36
38
|
}
|
|
37
39
|
export interface InboxPassResult {
|
|
@@ -52,6 +54,13 @@ export interface InboxPassResult {
|
|
|
52
54
|
* watermark was deliberately held, so the same window arrives again.
|
|
53
55
|
*/
|
|
54
56
|
failures: number;
|
|
57
|
+
/**
|
|
58
|
+
* Assigned work this host was not allowed to run: a delivery it cannot open
|
|
59
|
+
* or prepare, a task or decision whose execution was refused. Skipped, not
|
|
60
|
+
* failed: the server keeps it pending, stops ringing for it, and offers it
|
|
61
|
+
* again once it can run, so it neither holds the watermark nor slows the loop.
|
|
62
|
+
*/
|
|
63
|
+
blocked: number;
|
|
55
64
|
}
|
|
56
65
|
/**
|
|
57
66
|
* A capability probe waits on a `probe_*` chat reply, not inbox ack. Act
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { deliveryHandlingId, withWorkExecution } from '@ziggs-ai/api-client';
|
|
1
2
|
import { ContextReadClient, InboxClient, planInboxAck, planPartialInboxAck, } from '@ziggs-ai/api-client';
|
|
2
3
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
3
4
|
import { isRateLimited } from '@ziggs-ai/api-client';
|
|
@@ -64,9 +65,9 @@ function proposalIsMine(proposal, ownAgentId) {
|
|
|
64
65
|
async function deliverMessagesForChat(deps, news, refs, reader, attempt, handledIds) {
|
|
65
66
|
const pending = new Map(refs.map(ref => [ref.resourceId, ref]));
|
|
66
67
|
for (const id of pending.keys()) {
|
|
67
|
-
if (deps.seen.has(id)) {
|
|
68
|
+
if (deps.seen.has(deliveryHandlingId(pending.get(id)))) {
|
|
69
|
+
handledIds.add(deliveryHandlingId(pending.get(id)));
|
|
68
70
|
pending.delete(id);
|
|
69
|
-
handledIds.add(id);
|
|
70
71
|
}
|
|
71
72
|
}
|
|
72
73
|
if (!pending.size)
|
|
@@ -89,6 +90,9 @@ async function deliverMessagesForChat(deps, news, refs, reader, attempt, handled
|
|
|
89
90
|
throw err;
|
|
90
91
|
// This read explicitly establishes that these deliveries are inaccessible.
|
|
91
92
|
// The server also excludes out-of-reach rows from its acknowledgment guard.
|
|
93
|
+
// A context refusal (including changed audience) must be retried, never acknowledged as read.
|
|
94
|
+
if (refs.some(ref => ref.executionContext))
|
|
95
|
+
throw err;
|
|
92
96
|
for (const id of pending.keys())
|
|
93
97
|
handledIds.add(id);
|
|
94
98
|
return delivered;
|
|
@@ -104,11 +108,12 @@ async function deliverMessagesForChat(deps, news, refs, reader, attempt, handled
|
|
|
104
108
|
// Preserve the message's addressee. Assignment separately authorizes
|
|
105
109
|
// this carrier to handle it, including a person's stand-in.
|
|
106
110
|
assigneeId: deps.ownAgentId,
|
|
111
|
+
...(ref.responsibilityId ? { responsibilityId: ref.responsibilityId, executionContext: ref.executionContext } : {}),
|
|
107
112
|
...(ref.agreementId ? { agreementId: ref.agreementId } : {}),
|
|
108
113
|
}));
|
|
109
114
|
if (handled) {
|
|
110
115
|
delivered++;
|
|
111
|
-
handledIds.add(
|
|
116
|
+
handledIds.add(deliveryHandlingId(ref));
|
|
112
117
|
}
|
|
113
118
|
pending.delete(id);
|
|
114
119
|
}
|
|
@@ -191,18 +196,52 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
191
196
|
// and the one the window actually bites — releases as soon as it is done.
|
|
192
197
|
// Several, and the oldest is worked last, so the release waits for it: no
|
|
193
198
|
// worse than acking at the end, which is what this replaced.
|
|
199
|
+
// Blocked work is the server's to retry, not a handover that failed here:
|
|
200
|
+
// it stays pending without ringing, so holding the mark or backing off for
|
|
201
|
+
// it would only delay the work that can run.
|
|
202
|
+
const blocked = envelope.deliveries.filter(d => mine(d) && (d.executionBlocked || d.outOfReach)).length
|
|
203
|
+
+ (envelope.tasksAwaitingMe ?? []).filter(t => t.executionBlocked).length
|
|
204
|
+
+ envelope.proposalsAwaitingMe.filter(p => p.assignedAgentId === deps.ownAgentId && p.executionBlocked).length;
|
|
205
|
+
if (blocked) {
|
|
206
|
+
log(label, `${blocked} assignment(s) cannot run yet (no access or execution refused); left pending for the server to offer again`);
|
|
207
|
+
}
|
|
208
|
+
if (deps.onTaskControlCheck) {
|
|
209
|
+
const blockedTasks = new Set([
|
|
210
|
+
...envelope.deliveries.filter(d => mine(d) && d.kind === 'task-state' && d.executionBlocked && !d.outOfReach).map(d => d.resourceId),
|
|
211
|
+
...(envelope.tasksAwaitingMe ?? []).filter(t => t.executionBlocked).map(t => t.taskId),
|
|
212
|
+
]);
|
|
213
|
+
for (const taskId of blockedTasks) {
|
|
214
|
+
try {
|
|
215
|
+
await deps.onTaskControlCheck(taskId);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
warn(label, `task control observation failed for ${taskId}: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
194
222
|
const handledSoFar = new Set();
|
|
195
|
-
|
|
223
|
+
const acknowledged = new Set();
|
|
224
|
+
// A handled id the server would not settle (it lost access mid-wake) stays
|
|
225
|
+
// pending there and comes back once it can run; this pass is not redone.
|
|
226
|
+
const noteRefused = (result) => {
|
|
227
|
+
const refused = result ? result.refusedDeliveryIds : undefined;
|
|
228
|
+
if (refused?.length) {
|
|
229
|
+
warn(label, `ack left ${refused.length} handled assignment(s) pending: ${refused.map(r => `${r.id} (${r.reason})`).join(', ')}`);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
196
232
|
const ackWhatIsFinished = async () => {
|
|
197
233
|
const step = planPartialInboxAck(envelope, handledSoFar, {
|
|
198
234
|
ownAgentId: deps.ownAgentId,
|
|
199
235
|
failures,
|
|
200
236
|
});
|
|
201
|
-
if (!step
|
|
237
|
+
if (!step)
|
|
238
|
+
return;
|
|
239
|
+
step.handledDeliveryIds = step.handledDeliveryIds.filter(id => !acknowledged.has(id));
|
|
240
|
+
if (!step.handledDeliveryIds.length)
|
|
202
241
|
return;
|
|
203
242
|
try {
|
|
204
|
-
await inbox.ack(step.ackTo, {
|
|
205
|
-
|
|
243
|
+
noteRefused(await inbox.ack(step.ackTo, { handledDeliveryIds: step.handledDeliveryIds }));
|
|
244
|
+
step.handledDeliveryIds.forEach(id => acknowledged.add(id));
|
|
206
245
|
log(label, `acked up to ${step.ackTo} (mid-pass)`);
|
|
207
246
|
}
|
|
208
247
|
catch (err) {
|
|
@@ -214,22 +253,25 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
214
253
|
};
|
|
215
254
|
const assignedByChat = new Map();
|
|
216
255
|
for (const d of envelope.deliveries) {
|
|
217
|
-
if (d.kind !== 'message' || !d.chatId || !mine(d) || d.outOfReach)
|
|
256
|
+
if (d.kind !== 'message' || !d.chatId || !mine(d) || d.outOfReach || d.executionBlocked)
|
|
218
257
|
continue;
|
|
219
|
-
const
|
|
258
|
+
const groupId = d.executionContext?.contextId ?? d.chatId;
|
|
259
|
+
const rows = assignedByChat.get(groupId) ?? [];
|
|
220
260
|
rows.push(d);
|
|
221
|
-
assignedByChat.set(
|
|
261
|
+
assignedByChat.set(groupId, rows);
|
|
222
262
|
}
|
|
223
263
|
const chats = [...assignedByChat].map(([chatId, refs]) => ({
|
|
224
264
|
chatId, count: refs.length, latestAt: refs.map(ref => ref.ts).sort().at(-1),
|
|
225
265
|
}));
|
|
226
|
-
for (const
|
|
266
|
+
for (const group of chatsInWakeOrder(chats)) {
|
|
267
|
+
const refs = assignedByChat.get(group.chatId);
|
|
268
|
+
const news = { ...group, chatId: refs[0].chatId };
|
|
227
269
|
// Read a room's mail as that room's wake would: with the room as the lane.
|
|
228
270
|
// Without it the read is judged in the workspace the key signed in to, and
|
|
229
271
|
// a room another company opened with a listed agent is filed in theirs, so
|
|
230
272
|
// every stranger's message came back 403 and was acked unread.
|
|
231
|
-
const roomReader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl
|
|
232
|
-
const n = await deliverMessagesForChat(deps, news,
|
|
273
|
+
const roomReader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
274
|
+
const n = await withWorkExecution(refs[0]?.executionContext, () => deliverMessagesForChat(deps, news, refs, roomReader, attempt, handledSoFar));
|
|
233
275
|
messagesDelivered += n;
|
|
234
276
|
await ackWhatIsFinished();
|
|
235
277
|
}
|
|
@@ -243,13 +285,13 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
243
285
|
for (const d of envelope.deliveries) {
|
|
244
286
|
if (d.kind === 'message' || d.kind === 'request')
|
|
245
287
|
continue;
|
|
246
|
-
if (!mine(d))
|
|
288
|
+
if (!mine(d) || d.executionBlocked || d.outOfReach)
|
|
247
289
|
continue;
|
|
248
|
-
const key = `delivery:${d.kind}:${d.resourceId}:${d.ts}`;
|
|
290
|
+
const key = d.responsibilityId ?? `delivery:${d.kind}:${d.resourceId}:${d.ts}`;
|
|
249
291
|
// Handled on an earlier pass — it still settles this instant, exactly as
|
|
250
292
|
// it does in the end-of-pass list.
|
|
251
293
|
if (deps.seen.has(key)) {
|
|
252
|
-
handledSoFar.add(d
|
|
294
|
+
handledSoFar.add(deliveryHandlingId(d));
|
|
253
295
|
continue;
|
|
254
296
|
}
|
|
255
297
|
deps.seen.mark(key);
|
|
@@ -257,6 +299,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
257
299
|
kind: d.kind,
|
|
258
300
|
ts: d.ts,
|
|
259
301
|
resourceId: d.resourceId,
|
|
302
|
+
...(d.responsibilityId ? { responsibilityId: d.responsibilityId, executionContext: d.executionContext } : {}),
|
|
260
303
|
...(d.chatId ? { chatId: d.chatId } : {}),
|
|
261
304
|
...(d.agreementId ? { agreementId: d.agreementId } : {}),
|
|
262
305
|
...(d.taskId ? { taskId: d.taskId } : {}),
|
|
@@ -268,7 +311,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
268
311
|
}), () => deps.seen.unmark(key));
|
|
269
312
|
if (handled) {
|
|
270
313
|
resourceEvents++;
|
|
271
|
-
handledSoFar.add(d
|
|
314
|
+
handledSoFar.add(deliveryHandlingId(d));
|
|
272
315
|
}
|
|
273
316
|
}
|
|
274
317
|
// An older row here may have been holding the mark back behind the chats.
|
|
@@ -278,12 +321,14 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
278
321
|
// taskId+updatedAt — a genuinely touched task wakes again, an untouched one
|
|
279
322
|
// doesn't storm the lane every long-poll.
|
|
280
323
|
for (const task of envelope.tasksAwaitingMe ?? []) {
|
|
281
|
-
|
|
324
|
+
if (task.executionBlocked)
|
|
325
|
+
continue;
|
|
326
|
+
const key = `task:${task.taskId}:${task.updatedAt ?? ''}:${task.executionContext?.contextId ?? ''}`;
|
|
282
327
|
if (deps.seen.has(key))
|
|
283
328
|
continue;
|
|
284
329
|
deps.seen.mark(key);
|
|
285
330
|
if (deps.onTask) {
|
|
286
|
-
const handled = await attempt(`task ${task.taskId}`, () => deps.onTask(task), () => deps.seen.unmark(key));
|
|
331
|
+
const handled = await attempt(`task ${task.taskId}`, () => withWorkExecution(task.executionContext, () => deps.onTask(task)), () => deps.seen.unmark(key));
|
|
287
332
|
if (handled)
|
|
288
333
|
tasksDelivered++;
|
|
289
334
|
}
|
|
@@ -296,7 +341,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
296
341
|
for (const request of envelope.openRequestsAwaitingMe ?? []) {
|
|
297
342
|
const key = `request:${request.agreementId}:${request.ts}`;
|
|
298
343
|
if (deps.seen.has(key)) {
|
|
299
|
-
handledSoFar.add(request.
|
|
344
|
+
handledSoFar.add(request.responsibilityId);
|
|
300
345
|
continue;
|
|
301
346
|
}
|
|
302
347
|
deps.seen.mark(key);
|
|
@@ -304,25 +349,27 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
304
349
|
const handled = await attempt(`request ${request.agreementId}`, () => deps.onRequest(request), () => deps.seen.unmark(key));
|
|
305
350
|
if (handled) {
|
|
306
351
|
requestsTriaged++;
|
|
307
|
-
handledSoFar.add(request.
|
|
352
|
+
handledSoFar.add(request.responsibilityId);
|
|
308
353
|
}
|
|
309
354
|
}
|
|
310
355
|
else {
|
|
311
356
|
// No handler → still count as handled (ack) so an old host cannot wedge
|
|
312
357
|
// forever on request mail it does not understand.
|
|
313
358
|
requestsTriaged++;
|
|
314
|
-
handledSoFar.add(request.
|
|
359
|
+
handledSoFar.add(request.responsibilityId);
|
|
315
360
|
}
|
|
316
361
|
}
|
|
317
362
|
// Pending decisions are standing state, not deliveries: they keep appearing
|
|
318
363
|
// until answered, so dedup them the same way tasks are deduped.
|
|
319
364
|
for (const proposal of envelope.proposalsAwaitingMe) {
|
|
365
|
+
if (proposal.executionBlocked)
|
|
366
|
+
continue;
|
|
320
367
|
// A proposal may be visible because this agent shares the accountable
|
|
321
368
|
// principal's mailbox. Only the role-designated side actor wakes; ambient
|
|
322
369
|
// sibling proposals remain available as context without creating a lane.
|
|
323
370
|
if (!proposalIsMine(proposal, deps.ownAgentId))
|
|
324
371
|
continue;
|
|
325
|
-
const key = `proposal:${proposal.agreementId}`;
|
|
372
|
+
const key = `proposal:${proposal.agreementId}${proposal.executionContext ? ':' + proposal.executionContext.contextId : ''}`;
|
|
326
373
|
if (deps.seen.has(key))
|
|
327
374
|
continue;
|
|
328
375
|
deps.seen.mark(key);
|
|
@@ -333,6 +380,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
333
380
|
agreementId: proposal.agreementId,
|
|
334
381
|
change: 'created',
|
|
335
382
|
reason: 'proposal_pending',
|
|
383
|
+
...(proposal.executionContext ? { executionContext: proposal.executionContext } : {}),
|
|
336
384
|
}), () => deps.seen.unmark(key));
|
|
337
385
|
if (handled)
|
|
338
386
|
resourceEvents++;
|
|
@@ -344,14 +392,16 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
344
392
|
// Repeated failures never turn unread mail into an acknowledged delivery.
|
|
345
393
|
// One function, shared with the MCP inbox tool — do not restate the list
|
|
346
394
|
// or the allow-gate here.
|
|
347
|
-
let acked =
|
|
395
|
+
let acked = acknowledged.size > 0;
|
|
348
396
|
const ack = planInboxAck(envelope, {
|
|
349
397
|
ownAgentId: deps.ownAgentId,
|
|
350
398
|
failures,
|
|
351
399
|
forceAck: false,
|
|
352
400
|
});
|
|
353
|
-
if (ack.allowed && ack.
|
|
354
|
-
|
|
401
|
+
if (ack.allowed && ack.handledDeliveryIds.every(id => handledSoFar.has(id))) {
|
|
402
|
+
const remaining = ack.handledDeliveryIds.filter(id => !acknowledged.has(id));
|
|
403
|
+
if (remaining.length || !acked)
|
|
404
|
+
noteRefused(await inbox.ack(envelope.ackTo, { handledDeliveryIds: remaining }));
|
|
355
405
|
acked = true;
|
|
356
406
|
log(label, `acked up to ${envelope.ackTo}`);
|
|
357
407
|
}
|
|
@@ -368,6 +418,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
368
418
|
acked,
|
|
369
419
|
truncated: envelope.deliveriesCapped,
|
|
370
420
|
failures,
|
|
421
|
+
blocked,
|
|
371
422
|
};
|
|
372
423
|
}
|
|
373
424
|
/**
|
|
@@ -380,7 +431,7 @@ const ERROR_BACKOFF_BASE_MS = 1_000;
|
|
|
380
431
|
const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
381
432
|
/** Count held windows so retries can back off without discarding mail. */
|
|
382
433
|
export function heldPassAfter(prev, pass) {
|
|
383
|
-
return
|
|
434
|
+
return pass.failures > 0 || (!pass.acked && pass.truncated) ? prev + 1 : 0;
|
|
384
435
|
}
|
|
385
436
|
function sleep(ms, signal) {
|
|
386
437
|
return new Promise((resolve) => {
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
* `a:b` while the queue dispatches `a::b` simply never wakes anything, with
|
|
9
9
|
* nothing in the logs to say so.
|
|
10
10
|
*/
|
|
11
|
-
export declare function laneKeyFor(agentId: string, sessionId: string): string;
|
|
11
|
+
export declare function laneKeyFor(agentId: string, sessionId: string, executionContextId?: string): string;
|
package/dist/platform/laneKey.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { currentWorkExecution } from '@ziggs-ai/api-client';
|
|
1
2
|
/**
|
|
2
3
|
* The lane key for one agent's one conversation.
|
|
3
4
|
*
|
|
@@ -8,6 +9,6 @@
|
|
|
8
9
|
* `a:b` while the queue dispatches `a::b` simply never wakes anything, with
|
|
9
10
|
* nothing in the logs to say so.
|
|
10
11
|
*/
|
|
11
|
-
export function laneKeyFor(agentId, sessionId) {
|
|
12
|
-
return `${agentId}:${sessionId}`;
|
|
12
|
+
export function laneKeyFor(agentId, sessionId, executionContextId) {
|
|
13
|
+
return `${agentId}:${executionContextId ?? currentWorkExecution()?.contextId ?? sessionId}`;
|
|
13
14
|
}
|
|
@@ -24,7 +24,7 @@ export { PAYMENT_TOOLS, paymentBalanceTool } from './tier2/paymentTools.js';
|
|
|
24
24
|
export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, requestConnectionTool, } from './tier2/connectionTools.js';
|
|
25
25
|
export { CONTEXT_TOOLS, contextReadTool, accessRequestTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
|
|
26
26
|
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
27
|
-
export { TASK_TOOLS, listTasksTool, cancelTaskTool } from './tier2/taskTools.js';
|
|
27
|
+
export { TASK_TOOLS, listTasksTool, cancelTaskTool, delegateTaskTool } from './tier2/taskTools.js';
|
|
28
28
|
export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
|
|
29
29
|
export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
|
|
30
30
|
export { INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, } from './tier2/introductionTools.js';
|
|
@@ -28,7 +28,7 @@ export { CONTEXT_TOOLS, contextReadTool, accessRequestTool, contextDelegateTool,
|
|
|
28
28
|
// one grant lister across every rail (retires context_discover +
|
|
29
29
|
// payment_list_grants). Always-available on the hosted agent — see CLAUDE_SDK_TOOLS.
|
|
30
30
|
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
31
|
-
export { TASK_TOOLS, listTasksTool, cancelTaskTool } from './tier2/taskTools.js';
|
|
31
|
+
export { TASK_TOOLS, listTasksTool, cancelTaskTool, delegateTaskTool } from './tier2/taskTools.js';
|
|
32
32
|
export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
|
|
33
33
|
// links (agreements with engagementKind 'link') — cross-org reach.
|
|
34
34
|
///1022: the rail is two tools; propose/claim/revoke ride the
|
|
@@ -72,7 +72,9 @@ cachedAgreements) {
|
|
|
72
72
|
case 'agreement-respond':
|
|
73
73
|
if (!payload.agreementId || !payload.action)
|
|
74
74
|
return null;
|
|
75
|
-
return agreementService.respond(payload.agreementId, payload.action
|
|
75
|
+
return agreementService.respond(payload.agreementId, payload.action, typeof payload.mandateAgreementId === 'string' && payload.mandateAgreementId
|
|
76
|
+
? { mandateAgreementId: payload.mandateAgreementId }
|
|
77
|
+
: {});
|
|
76
78
|
case 'agreement-claim': {
|
|
77
79
|
// the one claim verb — request, standing offer, or link invite.
|
|
78
80
|
if (!payload.agreementId)
|
|
@@ -334,7 +336,7 @@ async function agreementsInChat(services, chatId, cachedAgreements) {
|
|
|
334
336
|
}
|
|
335
337
|
try {
|
|
336
338
|
// The room is the lane, as for every read made on a wake.
|
|
337
|
-
const reader = services.contextReadClient
|
|
339
|
+
const reader = services.contextReadClient;
|
|
338
340
|
const snap = await reader.snapshot(chatId, { maxMessages: 1 });
|
|
339
341
|
const ags = snap.agreements;
|
|
340
342
|
return Array.isArray(ags) ? ags : [];
|
|
@@ -66,7 +66,11 @@ export const agreementProposeTool = defineTool('agreement_propose', {
|
|
|
66
66
|
agreementDescription: 'string',
|
|
67
67
|
}, handlerFor('agreement_propose'), { description: 'Propose an agreement — direct, broadcast, or link; there are no separate publish tools. Engagement precedence (posted-first): 1) REUSE an active agreement on matching terms, 2) CLAIM the counterparty\'s listing (marketplace_view → agreement_claim; never counter a listing), 3) POST a request broadcast here when nothing listed fits, 4) direct-propose ONLY for bespoke terms, renegotiation, or commissioning a named counterparty with no listing — most published agents are claim-only and refuse direct proposals with a pointer at their listing. Direction is stated, never guessed: providerId = your own id ("I work, you pay") or providerId = proposedTo ("you work, I pay") on direct proposals. Broadcast with proposedTo="everyone" (fully public), "org" (your org only) or "company" (every team in your company) and no providerId posts a REQUEST any agent may claim. A person or the deploy publishes a standing listing; this agent does not. For a bilateral trust link, use link_propose: it takes an email or an agent id, or neither for a share link. engagementKind defaults to service. Agreements are STANDING by default (no expiry, unlimited tasks under one agreement); set expiresAt or maxExecutions only when the engagement should end on its own.', isAgreementCreation: true });
|
|
68
68
|
export const agreementSubcontractTool = defineTool('agreement_subcontract', agreementSubcontractCapability.params, handlerFor('agreement_subcontract'), { description: agreementSubcontractCapability.descriptions.sdk, isAgreementCreation: true });
|
|
69
|
-
export const agreementRespondTool = defineTool('agreement_respond', {
|
|
69
|
+
export const agreementRespondTool = defineTool('agreement_respond', {
|
|
70
|
+
agreementId: { type: 'string', required: true },
|
|
71
|
+
action: { type: 'string', required: true, enum: ['approve', 'reject'] },
|
|
72
|
+
mandateAgreementId: { type: 'string', description: 'The hire you act under, when its setting lets you answer without a person.' },
|
|
73
|
+
}, handlerFor('agreement_respond'), { description: 'Approve or reject a pending DIRECT agreement proposal addressed to you. Open broadcasts (requests, standing offers, link invites) have no personal approval slot — claim those with agreement_claim instead, or ignore them to pass. ONE exception: a hand-off that pins YOU as provider carries your pending approval even as an open broadcast — approving consents to serving whoever claims it (the row stays open for claims); rejecting cancels the hand-off. Ignoring it leaves the hand-off unclaimable, so always respond.' });
|
|
70
74
|
export const agreementClaimTool = defineTool('agreement_claim', agreementClaimCapability.params, handlerFor('agreement_claim'), { description: agreementClaimCapability.descriptions.sdk });
|
|
71
75
|
export const taskSetResultTool = defineTool('task_set_result', {
|
|
72
76
|
taskId: { type: 'string', required: true },
|
|
@@ -38,7 +38,7 @@ async function resolveTarget(ctx, toolName, connectionId, grantId) {
|
|
|
38
38
|
// No explicit baseUrl: ConnectionsClient already resolves it the way the fleet
|
|
39
39
|
// configures it (HTTP_URL). Passing BACKEND_URL here was the same mistake as in
|
|
40
40
|
// the transport — undefined in deployment.
|
|
41
|
-
const groups = await new ConnectionsClient(creds.operatorKey, creds.agentId, undefined
|
|
41
|
+
const groups = await new ConnectionsClient(creds.operatorKey, creds.agentId, undefined).listForHolder();
|
|
42
42
|
const live = groups.filter((g) => (g.grants ?? []).length > 0);
|
|
43
43
|
if (live.length === 0) {
|
|
44
44
|
throw new Error('No live connection grant. Ask the connection owner to issue one for this agent, then retry.');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
2
|
export declare const listTasksTool: ToolDefinition;
|
|
3
|
+
export declare const delegateTaskTool: ToolDefinition;
|
|
3
4
|
export declare const cancelTaskTool: ToolDefinition;
|
|
4
5
|
export declare const TASK_TOOLS: ToolDefinition[];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { cancelTaskCapability, listTasksCapability, TASK_CAPABILITIES } from '@ziggs-ai/api-client';
|
|
1
|
+
import { delegateTaskCapability, cancelTaskCapability, listTasksCapability, TASK_CAPABILITIES } from '@ziggs-ai/api-client';
|
|
2
2
|
import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
|
|
3
3
|
// GET /tasks, defined once in api-client. MCP ziggs_task_list and this
|
|
4
4
|
// hosted-Claude tool used to re-declare the same filters.
|
|
5
5
|
export const listTasksTool = toolFromCapability(listTasksCapability);
|
|
6
|
+
export const delegateTaskTool = toolFromCapability(delegateTaskCapability);
|
|
6
7
|
export const cancelTaskTool = toolFromCapability(cancelTaskCapability);
|
|
7
8
|
export const TASK_TOOLS = toolsFromCapabilities(TASK_CAPABILITIES);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ziggs-ai/agent-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.42.2",
|
|
4
4
|
"description": "Agent framework SDK for building autonomous agents on the Ziggs platform",
|
|
5
5
|
"homepage": "https://ziggsai.com/docs",
|
|
6
6
|
"bugs": {
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"@anthropic-ai/claude-agent-sdk": "^0.3.284",
|
|
31
31
|
"@anthropic-ai/sdk": "^0.110.0",
|
|
32
32
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
|
-
"@ziggs-ai/api-client": "0.
|
|
34
|
-
"@ziggs-ai/contracts": "^0.
|
|
33
|
+
"@ziggs-ai/api-client": "0.38.2",
|
|
34
|
+
"@ziggs-ai/contracts": "^0.33.1",
|
|
35
35
|
"ajv": "^8.17.1",
|
|
36
36
|
"dotenv": "^17.2.3",
|
|
37
37
|
"openai": "^4.0.0",
|