@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
|
@@ -1,9 +1,8 @@
|
|
|
1
|
+
import { prepareWorkExecution } from '@ziggs-ai/api-client';
|
|
1
2
|
import { deliveryHandlingId, withWorkExecution } from '@ziggs-ai/api-client';
|
|
2
|
-
import {
|
|
3
|
+
import { MessageRecordsClient, InboxClient, planInboxAck, planPartialInboxAck, } from '@ziggs-ai/api-client';
|
|
3
4
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
4
5
|
import { isRateLimited } from '@ziggs-ai/api-client';
|
|
5
|
-
import { isPublishProbeChat } from '../probeChat.js';
|
|
6
|
-
const READ_PAGE = 100;
|
|
7
6
|
function log(label, msg) {
|
|
8
7
|
runtimeLog.info(label, msg);
|
|
9
8
|
}
|
|
@@ -26,21 +25,6 @@ async function releaseInboxHostBestEffort(deps) {
|
|
|
26
25
|
function warn(label, msg) {
|
|
27
26
|
runtimeLog.warn(label, msg);
|
|
28
27
|
}
|
|
29
|
-
/**
|
|
30
|
-
* A capability probe waits on a `probe_*` chat reply, not inbox ack. Act
|
|
31
|
-
* those rooms before leftover mail so a standing host answers the gate
|
|
32
|
-
* even when the envelope also carries older chats. The prefix only orders
|
|
33
|
-
* the wake — it does not buy a canned answer.
|
|
34
|
-
*/
|
|
35
|
-
export function chatsInWakeOrder(chats) {
|
|
36
|
-
return [...chats].sort((a, b) => {
|
|
37
|
-
const ap = isPublishProbeChat(a.chatId) ? 0 : 1;
|
|
38
|
-
const bp = isPublishProbeChat(b.chatId) ? 0 : 1;
|
|
39
|
-
if (ap !== bp)
|
|
40
|
-
return ap - bp;
|
|
41
|
-
return a.latestAt < b.latestAt ? 1 : -1;
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
28
|
/**
|
|
45
29
|
* Standing proposals share the merged inbox visibility model with deliveries,
|
|
46
30
|
* but older envelopes predate an explicit assignee stamp. Prefer the server's
|
|
@@ -55,84 +39,41 @@ function proposalIsMine(proposal, ownAgentId) {
|
|
|
55
39
|
return (proposal.pendingApprovalPartyIds?.includes(ownAgentId) === true ||
|
|
56
40
|
proposal.proposedTo === ownAgentId);
|
|
57
41
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
handledIds.add(deliveryHandlingId(pending.get(id)));
|
|
70
|
-
pending.delete(id);
|
|
71
|
-
}
|
|
42
|
+
/** Read exact assigned records. Historical lookup is optional context, never handoff. */
|
|
43
|
+
async function deliverAssignedMessage(deps, ref, reader, attempt, handledIds) {
|
|
44
|
+
// A pending answer comes from its message owner, not from a delivery, so it
|
|
45
|
+
// has no receipt to settle. Its eventId still names that exact slot, which
|
|
46
|
+
// is enough to avoid a second wake for it.
|
|
47
|
+
const receiptId = ref.receiptId ? deliveryHandlingId(ref) : undefined;
|
|
48
|
+
const key = receiptId ?? ref.eventId;
|
|
49
|
+
if (deps.seen.has(key)) {
|
|
50
|
+
if (receiptId)
|
|
51
|
+
handledIds.add(receiptId);
|
|
52
|
+
return false;
|
|
72
53
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
catch (err) {
|
|
88
|
-
const status = err.status;
|
|
89
|
-
if (status !== 403 && status !== 404)
|
|
90
|
-
throw err;
|
|
91
|
-
// This read explicitly establishes that these deliveries are inaccessible.
|
|
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;
|
|
96
|
-
for (const id of pending.keys())
|
|
97
|
-
handledIds.add(id);
|
|
98
|
-
return delivered;
|
|
99
|
-
}
|
|
100
|
-
for (const row of page.items) {
|
|
101
|
-
const id = String(row.messageId ?? '');
|
|
102
|
-
const ref = pending.get(id);
|
|
103
|
-
if (!ref)
|
|
104
|
-
continue;
|
|
105
|
-
const handled = await attempt(`chat ${news.chatId} message ${id}`, () => deps.onMessage(typeof row.text === 'string' ? row.text : '', {
|
|
106
|
-
...row,
|
|
107
|
-
chatId: news.chatId,
|
|
108
|
-
// Preserve the message's addressee. Assignment separately authorizes
|
|
109
|
-
// this carrier to handle it, including a person's stand-in.
|
|
110
|
-
assigneeId: deps.ownAgentId,
|
|
111
|
-
...(ref.responsibilityId ? { responsibilityId: ref.responsibilityId, executionContext: ref.executionContext } : {}),
|
|
54
|
+
const handled = await attempt(`message ${ref.resourceId}`, () => (async () => {
|
|
55
|
+
if (!ref.action)
|
|
56
|
+
throw new Error('Assigned message has no domain action');
|
|
57
|
+
const execution = await prepareWorkExecution(ref.action, deps.creds.operatorKey, deps.ownAgentId, deps.baseUrl);
|
|
58
|
+
return withWorkExecution(execution, async () => {
|
|
59
|
+
const row = await reader.get(ref.resourceId);
|
|
60
|
+
const metadata = { messageId: row.messageId, ...(row.mandate ?? {}),
|
|
61
|
+
sender: { id: row.sender.ref, type: row.sender.kind }, receiver: { id: deps.ownAgentId, type: 'agent' },
|
|
62
|
+
entryType: 'message', timestamp: row.timestamp, roomless: true };
|
|
63
|
+
await deps.onMessage(row.content.text, {
|
|
64
|
+
...metadata, assigneeId: deps.ownAgentId, ...(receiptId ? { receiptId } : {}),
|
|
65
|
+
executionContext: execution,
|
|
112
66
|
...(ref.agreementId ? { agreementId: ref.agreementId } : {}),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
delivered++;
|
|
116
|
-
handledIds.add(deliveryHandlingId(ref));
|
|
117
|
-
}
|
|
118
|
-
pending.delete(id);
|
|
119
|
-
}
|
|
120
|
-
if (!page.hasMore)
|
|
121
|
-
break;
|
|
122
|
-
if (page.nextCursor && page.nextCursor !== cursor)
|
|
123
|
-
cursor = page.nextCursor;
|
|
124
|
-
else if (page.latestSequence && page.latestSequence > after) {
|
|
125
|
-
after = page.latestSequence;
|
|
126
|
-
cursor = undefined;
|
|
127
|
-
}
|
|
128
|
-
else
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
if (pending.size)
|
|
132
|
-
await attempt(`chat ${news.chatId} missing assigned messages`, async () => {
|
|
133
|
-
throw new Error(`Assigned message(s) not read: ${[...pending.keys()].join(', ')}`);
|
|
67
|
+
...(ref.taskId ? { taskId: ref.taskId } : {}),
|
|
68
|
+
});
|
|
134
69
|
});
|
|
135
|
-
|
|
70
|
+
})());
|
|
71
|
+
if (handled) {
|
|
72
|
+
deps.seen.mark(key);
|
|
73
|
+
if (receiptId)
|
|
74
|
+
handledIds.add(receiptId);
|
|
75
|
+
}
|
|
76
|
+
return handled;
|
|
136
77
|
}
|
|
137
78
|
/**
|
|
138
79
|
* One long-poll pass: hold up to `waitSeconds` for new inbox content, then read
|
|
@@ -181,7 +122,15 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
181
122
|
// Everything else in the merged view (a sibling carrier's mail, the owner's
|
|
182
123
|
// needs-human rows, granted ambient lanes) is context the host may read on
|
|
183
124
|
// demand — never a wake, never mine to ack as handled.
|
|
184
|
-
|
|
125
|
+
//
|
|
126
|
+
// A responsibility is its receipt: without one the server cannot settle the
|
|
127
|
+
// row, so it is not mine to act on. One such row (written by an older
|
|
128
|
+
// server) must not fail the whole pass and stop every other delivery.
|
|
129
|
+
const unreceipted = envelope.deliveries.filter(d => d.assigneeId === deps.ownAgentId && !d.receiptId);
|
|
130
|
+
if (unreceipted.length) {
|
|
131
|
+
warn(label, `${unreceipted.length} assigned deliver${unreceipted.length === 1 ? 'y has' : 'ies have'} no receiptId; skipped (${unreceipted.map(d => `${d.kind} ${d.resourceId}`).join(', ')})`);
|
|
132
|
+
}
|
|
133
|
+
const mine = (d) => d.assigneeId === deps.ownAgentId && Boolean(d.receiptId);
|
|
185
134
|
// Give the mark back in steps instead of once at the end of the pass.
|
|
186
135
|
//
|
|
187
136
|
// A pass that acks only when it finishes freezes the watermark for its whole
|
|
@@ -205,7 +154,23 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
205
154
|
if (blocked) {
|
|
206
155
|
log(label, `${blocked} assignment(s) cannot run yet (no access or execution refused); left pending for the server to offer again`);
|
|
207
156
|
}
|
|
208
|
-
|
|
157
|
+
if (deps.onTaskControlCheck) {
|
|
158
|
+
const blockedTasks = new Set([
|
|
159
|
+
...envelope.deliveries.filter(d => mine(d) && d.kind === 'task-state' && d.executionBlocked && !d.outOfReach).map(d => d.resourceId),
|
|
160
|
+
...(envelope.tasksAwaitingMe ?? []).filter(t => t.executionBlocked).map(t => t.taskId),
|
|
161
|
+
]);
|
|
162
|
+
for (const taskId of blockedTasks) {
|
|
163
|
+
try {
|
|
164
|
+
await deps.onTaskControlCheck(taskId);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
warn(label, `task control observation failed for ${taskId}: ${error.message}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// A receipt with no owner action is transport-only: acknowledge observation
|
|
172
|
+
// without creating model work from an FYI.
|
|
173
|
+
const handledSoFar = new Set(envelope.deliveries.filter(d => mine(d) && d.kind !== 'request' && (!d.action || d.actionState === 'settled') && !d.outOfReach && !d.executionBlocked).map(deliveryHandlingId));
|
|
209
174
|
const acknowledged = new Set();
|
|
210
175
|
// A handled id the server would not settle (it lost access mid-wake) stays
|
|
211
176
|
// pending there and comes back once it can run; this pass is not redone.
|
|
@@ -237,43 +202,36 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
237
202
|
warn(label, `mid-pass ack did not land: ${err?.message ?? err}`);
|
|
238
203
|
}
|
|
239
204
|
};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
const chats = [...assignedByChat].map(([chatId, refs]) => ({
|
|
250
|
-
chatId, count: refs.length, latestAt: refs.map(ref => ref.ts).sort().at(-1),
|
|
251
|
-
}));
|
|
252
|
-
for (const group of chatsInWakeOrder(chats)) {
|
|
253
|
-
const refs = assignedByChat.get(group.chatId);
|
|
254
|
-
const news = { ...group, chatId: refs[0].chatId };
|
|
255
|
-
// Read a room's mail as that room's wake would: with the room as the lane.
|
|
256
|
-
// Without it the read is judged in the workspace the key signed in to, and
|
|
257
|
-
// a room another company opened with a listed agent is filed in theirs, so
|
|
258
|
-
// every stranger's message came back 403 and was acked unread.
|
|
259
|
-
const roomReader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
260
|
-
const n = await withWorkExecution(refs[0]?.executionContext, () => deliverMessagesForChat(deps, news, refs, roomReader, attempt, handledSoFar));
|
|
261
|
-
messagesDelivered += n;
|
|
205
|
+
// Every assigned message is fetched by resource identity. Navigation is optional.
|
|
206
|
+
const assigned = envelope.deliveries
|
|
207
|
+
.filter(d => d.kind === 'message' && !!d.action && d.action.kind !== 'task' && d.actionState !== 'settled' && d.actionState !== 'waiting' && mine(d) && !d.outOfReach && !d.executionBlocked)
|
|
208
|
+
.sort((a, b) => b.ts.localeCompare(a.ts));
|
|
209
|
+
const records = new MessageRecordsClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
210
|
+
for (const delivery of assigned) {
|
|
211
|
+
if (await deliverAssignedMessage(deps, delivery, records, attempt, handledSoFar))
|
|
212
|
+
messagesDelivered++;
|
|
262
213
|
await ackWhatIsFinished();
|
|
263
214
|
}
|
|
215
|
+
// Recover owner actions even after their transport receipt was acknowledged.
|
|
216
|
+
const deliveredAnswerKeys = new Set(assigned.map(delivery => JSON.stringify(delivery.action)));
|
|
217
|
+
for (const answer of envelope.answersAwaitingMe ?? []) {
|
|
218
|
+
if (!answer.action || answer.executionBlocked || answer.outOfReach || deliveredAnswerKeys.has(JSON.stringify(answer.action)))
|
|
219
|
+
continue;
|
|
220
|
+
if (await deliverAssignedMessage(deps, answer, records, attempt, new Set()))
|
|
221
|
+
messagesDelivered++;
|
|
222
|
+
}
|
|
264
223
|
// Everything that is not chat text is handed over as a resource event, from
|
|
265
224
|
// the delivery itself — the reference already carries the ids the host needs,
|
|
266
225
|
// so nothing has to be re-derived to name what changed.
|
|
267
|
-
// Requests
|
|
268
|
-
// left them in `deliveries`, skip so they cannot wake the LLM before triage.
|
|
226
|
+
// Requests have a dedicated triage path and tasks use their canonical executor.
|
|
269
227
|
// Unassigned rows never take it either: waking the brain on a sibling's
|
|
270
228
|
// mail is the N-wakes-per-event cost the party-owned inbox exists to kill.
|
|
271
229
|
for (const d of envelope.deliveries) {
|
|
272
|
-
if (d.kind === 'message' || d.kind === 'request')
|
|
230
|
+
if (d.kind === 'message' || d.kind === 'request' || d.action?.kind === 'task')
|
|
273
231
|
continue;
|
|
274
|
-
if (!mine(d) || d.executionBlocked || d.outOfReach)
|
|
232
|
+
if (!mine(d) || d.executionBlocked || d.outOfReach || !d.action || d.actionState === 'settled' || d.actionState === 'waiting')
|
|
275
233
|
continue;
|
|
276
|
-
const key = d
|
|
234
|
+
const key = deliveryHandlingId(d);
|
|
277
235
|
// Handled on an earlier pass — it still settles this instant, exactly as
|
|
278
236
|
// it does in the end-of-pass list.
|
|
279
237
|
if (deps.seen.has(key)) {
|
|
@@ -281,20 +239,24 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
281
239
|
continue;
|
|
282
240
|
}
|
|
283
241
|
deps.seen.mark(key);
|
|
284
|
-
const handled = await attempt(`${d.kind} ${d.resourceId}`, () =>
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
242
|
+
const handled = await attempt(`${d.kind} ${d.resourceId}`, async () => {
|
|
243
|
+
const executionContext = await prepareWorkExecution(d.action, deps.creds.operatorKey, deps.ownAgentId, deps.baseUrl);
|
|
244
|
+
return deps.onResourceEvent({
|
|
245
|
+
kind: d.kind,
|
|
246
|
+
ts: d.ts,
|
|
247
|
+
resourceId: d.resourceId,
|
|
248
|
+
...(d.receiptId ? { receiptId: d.receiptId, executionContext } : {}),
|
|
249
|
+
...(d.addressId ? { addressId: d.addressId } : {}),
|
|
250
|
+
...(d.agreementId ? { agreementId: d.agreementId } : {}),
|
|
251
|
+
...(d.taskId ? { taskId: d.taskId } : {}),
|
|
252
|
+
// claim/listing wakes carry reason + claimer; host needs
|
|
253
|
+
// both for agrn lane routing, claim kickoff, and chat_open buyer id.
|
|
254
|
+
...(d.reason != null && d.reason !== '' ? { reason: d.reason } : {}),
|
|
255
|
+
...(d.actorId ? { actorId: d.actorId } : {}),
|
|
256
|
+
change: 'created',
|
|
257
|
+
executionContext,
|
|
258
|
+
});
|
|
259
|
+
}, () => deps.seen.unmark(key));
|
|
298
260
|
if (handled) {
|
|
299
261
|
resourceEvents++;
|
|
300
262
|
handledSoFar.add(deliveryHandlingId(d));
|
|
@@ -302,23 +264,65 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
302
264
|
}
|
|
303
265
|
// An older row here may have been holding the mark back behind the chats.
|
|
304
266
|
await ackWhatIsFinished();
|
|
305
|
-
//
|
|
306
|
-
//
|
|
307
|
-
|
|
308
|
-
// doesn't storm the lane every long-poll.
|
|
267
|
+
// One canonical task action, however many receipts announced changes to it.
|
|
268
|
+
// A receipt is acknowledged only after this action was handed to the task executor.
|
|
269
|
+
const taskActions = new Map();
|
|
309
270
|
for (const task of envelope.tasksAwaitingMe ?? []) {
|
|
271
|
+
taskActions.set(task.taskId, { task, receipts: [] });
|
|
272
|
+
}
|
|
273
|
+
for (const receipt of envelope.deliveries) {
|
|
274
|
+
if (!mine(receipt) || receipt.action?.kind !== 'task' || receipt.outOfReach)
|
|
275
|
+
continue;
|
|
276
|
+
if (receipt.actionState === 'settled') {
|
|
277
|
+
// Domain state ended or reassigned this work. No model execution is needed.
|
|
278
|
+
handledSoFar.add(deliveryHandlingId(receipt));
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (receipt.executionBlocked || receipt.actionState !== 'ready')
|
|
282
|
+
continue;
|
|
283
|
+
const id = receipt.action.id;
|
|
284
|
+
const existing = taskActions.get(id);
|
|
285
|
+
const task = {
|
|
286
|
+
taskId: id, agreementId: receipt.agreementId,
|
|
287
|
+
title: existing?.task.title ?? id, state: 'active',
|
|
288
|
+
updatedAt: receipt.actionVersion ?? null,
|
|
289
|
+
executionContext: receipt.executionContext,
|
|
290
|
+
};
|
|
291
|
+
if (!existing)
|
|
292
|
+
taskActions.set(id, { task, receipts: [receipt] });
|
|
293
|
+
else {
|
|
294
|
+
existing.receipts.push(receipt);
|
|
295
|
+
// Prefer the freshest owner snapshot, not the event's source timestamp.
|
|
296
|
+
if ((task.updatedAt ?? '') >= (existing.task.updatedAt ?? ''))
|
|
297
|
+
existing.task = task;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
for (const { task, receipts } of taskActions.values()) {
|
|
310
301
|
if (task.executionBlocked)
|
|
311
302
|
continue;
|
|
312
303
|
const key = `task:${task.taskId}:${task.updatedAt ?? ''}:${task.executionContext?.contextId ?? ''}`;
|
|
313
|
-
|
|
304
|
+
const receiptKeys = receipts.map(receipt => `task-receipt:${deliveryHandlingId(receipt)}`);
|
|
305
|
+
// A timestamp is not a causal sequence: a new input can share its owner's
|
|
306
|
+
// version with an earlier handoff. Only suppress receipts actually handled.
|
|
307
|
+
if (deps.seen.has(key) && receiptKeys.every(id => deps.seen.has(id))) {
|
|
308
|
+
receipts.forEach(receipt => handledSoFar.add(deliveryHandlingId(receipt)));
|
|
314
309
|
continue;
|
|
315
|
-
deps.seen.mark(key);
|
|
316
|
-
if (deps.onTask) {
|
|
317
|
-
const handled = await attempt(`task ${task.taskId}`, () => withWorkExecution(task.executionContext, () => deps.onTask(task)), () => deps.seen.unmark(key));
|
|
318
|
-
if (handled)
|
|
319
|
-
tasksDelivered++;
|
|
320
310
|
}
|
|
311
|
+
if (!deps.onTask)
|
|
312
|
+
continue; // Unsupported work stays pending; delivery is not completion.
|
|
313
|
+
const handled = await attempt(`task ${task.taskId}`, async () => {
|
|
314
|
+
const executionContext = await prepareWorkExecution({ kind: 'task', id: task.taskId }, deps.creds.operatorKey, deps.ownAgentId, deps.baseUrl);
|
|
315
|
+
return withWorkExecution(executionContext, () => deps.onTask({ ...task, executionContext }, receipts.map(receipt => ({ kind: receipt.kind, id: receipt.resourceId, eventId: receipt.eventId ?? receipt.receiptId }))));
|
|
316
|
+
});
|
|
317
|
+
if (handled) {
|
|
318
|
+
deps.seen.mark(key);
|
|
319
|
+
receiptKeys.forEach(id => deps.seen.mark(id));
|
|
320
|
+
tasksDelivered++;
|
|
321
|
+
receipts.forEach(receipt => handledSoFar.add(deliveryHandlingId(receipt)));
|
|
322
|
+
}
|
|
323
|
+
await ackWhatIsFinished();
|
|
321
324
|
}
|
|
325
|
+
await ackWhatIsFinished();
|
|
322
326
|
if (envelope.truncatedTasks) {
|
|
323
327
|
warn(label, `inbox truncatedTasks=${envelope.truncatedTasks} (more assigned tasks than one pass surfaces — they appear as earlier ones close)`);
|
|
324
328
|
}
|
|
@@ -327,7 +331,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
327
331
|
for (const request of envelope.openRequestsAwaitingMe ?? []) {
|
|
328
332
|
const key = `request:${request.agreementId}:${request.ts}`;
|
|
329
333
|
if (deps.seen.has(key)) {
|
|
330
|
-
handledSoFar.add(request.
|
|
334
|
+
handledSoFar.add(request.receiptId);
|
|
331
335
|
continue;
|
|
332
336
|
}
|
|
333
337
|
deps.seen.mark(key);
|
|
@@ -335,14 +339,14 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
335
339
|
const handled = await attempt(`request ${request.agreementId}`, () => deps.onRequest(request), () => deps.seen.unmark(key));
|
|
336
340
|
if (handled) {
|
|
337
341
|
requestsTriaged++;
|
|
338
|
-
handledSoFar.add(request.
|
|
342
|
+
handledSoFar.add(request.receiptId);
|
|
339
343
|
}
|
|
340
344
|
}
|
|
341
345
|
else {
|
|
342
346
|
// No handler → still count as handled (ack) so an old host cannot wedge
|
|
343
347
|
// forever on request mail it does not understand.
|
|
344
348
|
requestsTriaged++;
|
|
345
|
-
handledSoFar.add(request.
|
|
349
|
+
handledSoFar.add(request.receiptId);
|
|
346
350
|
}
|
|
347
351
|
}
|
|
348
352
|
// Pending decisions are standing state, not deliveries: they keep appearing
|
|
@@ -359,15 +363,18 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
359
363
|
if (deps.seen.has(key))
|
|
360
364
|
continue;
|
|
361
365
|
deps.seen.mark(key);
|
|
362
|
-
const handled = await attempt(`pending proposal ${proposal.agreementId}`, () =>
|
|
363
|
-
kind: 'agreement',
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
366
|
+
const handled = await attempt(`pending proposal ${proposal.agreementId}`, async () => {
|
|
367
|
+
const executionContext = await prepareWorkExecution({ kind: 'agreement', id: proposal.agreementId }, deps.creds.operatorKey, deps.ownAgentId, deps.baseUrl);
|
|
368
|
+
return deps.onResourceEvent({
|
|
369
|
+
kind: 'agreement',
|
|
370
|
+
ts: proposal.proposedAt ?? new Date().toISOString(),
|
|
371
|
+
resourceId: proposal.agreementId,
|
|
372
|
+
agreementId: proposal.agreementId,
|
|
373
|
+
change: 'created',
|
|
374
|
+
reason: 'proposal_pending',
|
|
375
|
+
executionContext,
|
|
376
|
+
});
|
|
377
|
+
}, () => deps.seen.unmark(key));
|
|
371
378
|
if (handled)
|
|
372
379
|
resourceEvents++;
|
|
373
380
|
}
|
|
@@ -9,39 +9,36 @@
|
|
|
9
9
|
* both inbox delivery and external polls; disconnect stops both. The process
|
|
10
10
|
* runner does not own a second copy of the polling lifecycle.
|
|
11
11
|
*
|
|
12
|
-
* A
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* A source may report an existing task action for authorized host processing.
|
|
13
|
+
* A mechanical source may dispatch through its existing domain owner and return
|
|
14
|
+
* actionable:false; the resulting owner delivery supplies work to its assignee.
|
|
15
|
+
* It never creates a room or borrows the execution that started the timer.
|
|
16
16
|
*
|
|
17
17
|
* WHERE THE CURSOR LIVES: in the source, whenever the source has one. Sente
|
|
18
18
|
* has `archived` on a message, so "already handled" is a fact about the
|
|
19
19
|
* mailbox, not something we mirror. A kind whose source has no such notion has
|
|
20
20
|
* to keep a cursor, and then it is the kind's business — never the agent's.
|
|
21
21
|
*/
|
|
22
|
-
/**
|
|
23
|
-
* Kind-specific config, verbatim from the agent's manifest.
|
|
24
|
-
*
|
|
25
|
-
* `chatId` is not kind-specific and is not optional in practice: a wake has to
|
|
26
|
-
* resolve to a lane or Agent's session resolver returns null and drops it
|
|
27
|
-
* silently. A poll has no engagement behind it to derive one from, so the
|
|
28
|
-
* binding names the chat the agent thinks in — which also puts its triage
|
|
29
|
-
* somewhere a human can watch. PollRunner refuses to register a binding without
|
|
30
|
-
* one rather than let the wake evaporate at runtime.
|
|
31
|
-
*/
|
|
22
|
+
/** Kind-specific source configuration. It carries no room or execution authority. */
|
|
32
23
|
export interface PollBinding {
|
|
33
24
|
kind: string;
|
|
34
|
-
/** Chat the poll wake is delivered into. Required; see above. */
|
|
35
|
-
chatId?: string;
|
|
36
25
|
[key: string]: unknown;
|
|
37
26
|
}
|
|
38
27
|
export interface PollContext {
|
|
39
28
|
agentId: string;
|
|
40
29
|
operatorKey?: string;
|
|
30
|
+
/** False after disconnect, including an older connection after reconnect. */
|
|
31
|
+
isActive(): boolean;
|
|
41
32
|
}
|
|
42
33
|
export interface PollResult {
|
|
43
34
|
/** Whether the agent should be woken now. */
|
|
44
35
|
actionable: boolean;
|
|
36
|
+
/** A model wake must name existing task work; owner preparation can refuse it.
|
|
37
|
+
* Mechanical sources dispatch through their owner and return actionable:false. */
|
|
38
|
+
action?: {
|
|
39
|
+
kind: 'task';
|
|
40
|
+
id: string;
|
|
41
|
+
};
|
|
45
42
|
/**
|
|
46
43
|
* One line telling the agent why it woke. Carries no payload — see above.
|
|
47
44
|
* Omit and the runner sends the kind's default.
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* both inbox delivery and external polls; disconnect stops both. The process
|
|
10
10
|
* runner does not own a second copy of the polling lifecycle.
|
|
11
11
|
*
|
|
12
|
-
* A
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* A source may report an existing task action for authorized host processing.
|
|
13
|
+
* A mechanical source may dispatch through its existing domain owner and return
|
|
14
|
+
* actionable:false; the resulting owner delivery supplies work to its assignee.
|
|
15
|
+
* It never creates a room or borrows the execution that started the timer.
|
|
16
16
|
*
|
|
17
17
|
* WHERE THE CURSOR LIVES: in the source, whenever the source has one. Sente
|
|
18
18
|
* has `archived` on a message, so "already handled" is a fact about the
|
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
* PollRunner — executes declared external wake sources for agents.
|
|
3
|
-
*
|
|
4
|
-
* Each Agent owns its runner and starts it with its inbox loop. A poll is per
|
|
5
|
-
* (agent, binding), with its own interval, backoff and backpressure. This same
|
|
6
|
-
* lifecycle applies to fleet, MCP-tool, lab and evaluation agents.
|
|
7
|
-
*
|
|
8
|
-
* One loop per (agent, binding), and each loop AWAITS its own wake before
|
|
9
|
-
* scheduling the next tick. That is the whole backpressure story: while an
|
|
10
|
-
* agent is still dealing with what it was woken for, its next poll has not
|
|
11
|
-
* happened yet, so it cannot be woken twice for the same thing. No in-flight
|
|
12
|
-
* bookkeeping, no dedup table — the source is authoritative and a poll that
|
|
13
|
-
* has not run yet cannot report anything stale.
|
|
14
|
-
*
|
|
15
|
-
* A failing source backs off exponentially and recovers on the first success.
|
|
16
|
-
* Without it a source that is down is hit at full rate indefinitely, which is
|
|
17
|
-
* how you get rate-limited by the thing you were trying to read.
|
|
18
|
-
*/
|
|
1
|
+
import { type WorkExecutionContext } from '@ziggs-ai/api-client';
|
|
19
2
|
import { type PollRegistry } from './PollKind.js';
|
|
20
|
-
/**
|
|
3
|
+
/** Existing task-owner wake entry; called only after exact execution preparation. */
|
|
21
4
|
export interface WakeTarget {
|
|
22
|
-
|
|
5
|
+
wakeTask(agentId: string, taskId: string, execution: WorkExecutionContext, opts?: {
|
|
23
6
|
timeoutMs?: number;
|
|
24
7
|
}): Promise<unknown>;
|
|
25
8
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { prepareWorkExecution, withAutonomousRequest, withWorkExecution } from '@ziggs-ai/api-client';
|
|
1
2
|
/**
|
|
2
3
|
* PollRunner — executes declared external wake sources for agents.
|
|
3
4
|
*
|
|
@@ -49,19 +50,11 @@ export class PollRunner {
|
|
|
49
50
|
runtimeLog.warn(label, `${agentId} declares unknown poll kind "${binding.kind}" — it will never be woken by it (registered: ${registry.ids.join(', ') || 'none'})`);
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
52
|
-
if (!binding.chatId) {
|
|
53
|
-
// Refused at startup rather than dropped per-wake: without a lane the
|
|
54
|
-
// host's session resolver returns null and the wake disappears with
|
|
55
|
-
// only a debug line, which looks exactly like "the source is quiet".
|
|
56
|
-
runtimeLog.warn(label, `${agentId} declares poll "${binding.kind}" without a chatId — refusing to register it, because every wake would be silently dropped`);
|
|
57
|
-
continue;
|
|
58
|
-
}
|
|
59
53
|
this.loops.push({
|
|
60
54
|
agentId,
|
|
61
55
|
binding,
|
|
62
56
|
kind,
|
|
63
57
|
intervalMs: kind.intervalMs ?? defaultIntervalMs,
|
|
64
|
-
chatId: binding.chatId,
|
|
65
58
|
timer: null,
|
|
66
59
|
failures: 0,
|
|
67
60
|
inFlight: false,
|
|
@@ -104,7 +97,8 @@ export class PollRunner {
|
|
|
104
97
|
loop.timer = setTimeout(() => {
|
|
105
98
|
loop.timer = null;
|
|
106
99
|
loop.inFlight = true;
|
|
107
|
-
|
|
100
|
+
const generation = this.generation;
|
|
101
|
+
void withAutonomousRequest(() => this.tick(loop, generation)).finally(() => {
|
|
108
102
|
loop.inFlight = false;
|
|
109
103
|
this.schedule(loop);
|
|
110
104
|
});
|
|
@@ -123,6 +117,7 @@ export class PollRunner {
|
|
|
123
117
|
try {
|
|
124
118
|
result = await loop.kind.poll(loop.binding, {
|
|
125
119
|
agentId: loop.agentId,
|
|
120
|
+
isActive: () => this.running && generation === this.generation,
|
|
126
121
|
...(this.operatorKey ? { operatorKey: this.operatorKey } : {}),
|
|
127
122
|
});
|
|
128
123
|
loop.failures = 0;
|
|
@@ -136,13 +131,16 @@ export class PollRunner {
|
|
|
136
131
|
if (!this.running || generation !== this.generation)
|
|
137
132
|
return;
|
|
138
133
|
if (result?.actionable && this.claimWake(loop)) {
|
|
139
|
-
const hint = result.hint ?? `New work from ${loop.kind.id}. Check it.`;
|
|
140
134
|
try {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
135
|
+
if (!this.operatorKey || result.action?.kind !== 'task' || !result.action.id?.trim()) {
|
|
136
|
+
throw new Error('Poll model wake requires an authenticated exact task action; unbound wake refused');
|
|
137
|
+
}
|
|
138
|
+
const execution = await prepareWorkExecution(result.action, this.operatorKey, loop.agentId);
|
|
139
|
+
if (!this.running || generation !== this.generation)
|
|
140
|
+
return;
|
|
141
|
+
if (execution.taskId !== result.action.id)
|
|
142
|
+
throw new Error('Poll preparation returned different task work');
|
|
143
|
+
await withWorkExecution(execution, () => this.pool.wakeTask(loop.agentId, result.action.id, execution, { timeoutMs: this.wakeTimeoutMs }));
|
|
146
144
|
runtimeLog.info(this.label, `woke ${loop.agentId} for ${loop.kind.id}`);
|
|
147
145
|
}
|
|
148
146
|
catch (err) {
|
|
@@ -4,7 +4,7 @@ import { PollRunner, type WakeTarget } from './PollRunner.js';
|
|
|
4
4
|
* Build the runner that executes this agent's declared wakes.
|
|
5
5
|
*
|
|
6
6
|
* A malformed `polls` value is a programming error and still throws. A
|
|
7
|
-
* binding that names an unknown kind
|
|
7
|
+
* binding that names an unknown kind is not: PollRunner
|
|
8
8
|
* warns and skips it, so one agent's invalid poll cannot refuse the host.
|
|
9
9
|
*/
|
|
10
10
|
export declare function createDeclaredPollRunner(options: AgentOptions, target: WakeTarget): PollRunner | null;
|
|
@@ -4,7 +4,7 @@ import { PollRunner } from './PollRunner.js';
|
|
|
4
4
|
* Build the runner that executes this agent's declared wakes.
|
|
5
5
|
*
|
|
6
6
|
* A malformed `polls` value is a programming error and still throws. A
|
|
7
|
-
* binding that names an unknown kind
|
|
7
|
+
* binding that names an unknown kind is not: PollRunner
|
|
8
8
|
* warns and skips it, so one agent's invalid poll cannot refuse the host.
|
|
9
9
|
*/
|
|
10
10
|
export function createDeclaredPollRunner(options, target) {
|