@ziggs-ai/agent-sdk 0.42.2 → 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 +38 -0
- package/README.md +1 -1
- package/dist/Agent.d.ts +21 -8
- package/dist/Agent.js +173 -97
- 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 +3 -3
- package/dist/brain/claudeSdkTools.js +37 -133
- 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 +3 -9
- package/dist/platform/inboxLoop.js +151 -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 +0 -1
- package/dist/platform/tools/index.js +0 -3
- 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/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
|
|
@@ -219,7 +168,9 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
219
168
|
}
|
|
220
169
|
}
|
|
221
170
|
}
|
|
222
|
-
|
|
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));
|
|
223
174
|
const acknowledged = new Set();
|
|
224
175
|
// A handled id the server would not settle (it lost access mid-wake) stays
|
|
225
176
|
// pending there and comes back once it can run; this pass is not redone.
|
|
@@ -251,43 +202,36 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
251
202
|
warn(label, `mid-pass ack did not land: ${err?.message ?? err}`);
|
|
252
203
|
}
|
|
253
204
|
};
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
}
|
|
263
|
-
const chats = [...assignedByChat].map(([chatId, refs]) => ({
|
|
264
|
-
chatId, count: refs.length, latestAt: refs.map(ref => ref.ts).sort().at(-1),
|
|
265
|
-
}));
|
|
266
|
-
for (const group of chatsInWakeOrder(chats)) {
|
|
267
|
-
const refs = assignedByChat.get(group.chatId);
|
|
268
|
-
const news = { ...group, chatId: refs[0].chatId };
|
|
269
|
-
// Read a room's mail as that room's wake would: with the room as the lane.
|
|
270
|
-
// Without it the read is judged in the workspace the key signed in to, and
|
|
271
|
-
// a room another company opened with a listed agent is filed in theirs, so
|
|
272
|
-
// every stranger's message came back 403 and was acked unread.
|
|
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));
|
|
275
|
-
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++;
|
|
276
213
|
await ackWhatIsFinished();
|
|
277
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
|
+
}
|
|
278
223
|
// Everything that is not chat text is handed over as a resource event, from
|
|
279
224
|
// the delivery itself — the reference already carries the ids the host needs,
|
|
280
225
|
// so nothing has to be re-derived to name what changed.
|
|
281
|
-
// Requests
|
|
282
|
-
// 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.
|
|
283
227
|
// Unassigned rows never take it either: waking the brain on a sibling's
|
|
284
228
|
// mail is the N-wakes-per-event cost the party-owned inbox exists to kill.
|
|
285
229
|
for (const d of envelope.deliveries) {
|
|
286
|
-
if (d.kind === 'message' || d.kind === 'request')
|
|
230
|
+
if (d.kind === 'message' || d.kind === 'request' || d.action?.kind === 'task')
|
|
287
231
|
continue;
|
|
288
|
-
if (!mine(d) || d.executionBlocked || d.outOfReach)
|
|
232
|
+
if (!mine(d) || d.executionBlocked || d.outOfReach || !d.action || d.actionState === 'settled' || d.actionState === 'waiting')
|
|
289
233
|
continue;
|
|
290
|
-
const key = d
|
|
234
|
+
const key = deliveryHandlingId(d);
|
|
291
235
|
// Handled on an earlier pass — it still settles this instant, exactly as
|
|
292
236
|
// it does in the end-of-pass list.
|
|
293
237
|
if (deps.seen.has(key)) {
|
|
@@ -295,20 +239,24 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
295
239
|
continue;
|
|
296
240
|
}
|
|
297
241
|
deps.seen.mark(key);
|
|
298
|
-
const handled = await attempt(`${d.kind} ${d.resourceId}`, () =>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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));
|
|
312
260
|
if (handled) {
|
|
313
261
|
resourceEvents++;
|
|
314
262
|
handledSoFar.add(deliveryHandlingId(d));
|
|
@@ -316,23 +264,65 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
316
264
|
}
|
|
317
265
|
// An older row here may have been holding the mark back behind the chats.
|
|
318
266
|
await ackWhatIsFinished();
|
|
319
|
-
//
|
|
320
|
-
//
|
|
321
|
-
|
|
322
|
-
// 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();
|
|
323
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()) {
|
|
324
301
|
if (task.executionBlocked)
|
|
325
302
|
continue;
|
|
326
303
|
const key = `task:${task.taskId}:${task.updatedAt ?? ''}:${task.executionContext?.contextId ?? ''}`;
|
|
327
|
-
|
|
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)));
|
|
328
309
|
continue;
|
|
329
|
-
deps.seen.mark(key);
|
|
330
|
-
if (deps.onTask) {
|
|
331
|
-
const handled = await attempt(`task ${task.taskId}`, () => withWorkExecution(task.executionContext, () => deps.onTask(task)), () => deps.seen.unmark(key));
|
|
332
|
-
if (handled)
|
|
333
|
-
tasksDelivered++;
|
|
334
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();
|
|
335
324
|
}
|
|
325
|
+
await ackWhatIsFinished();
|
|
336
326
|
if (envelope.truncatedTasks) {
|
|
337
327
|
warn(label, `inbox truncatedTasks=${envelope.truncatedTasks} (more assigned tasks than one pass surfaces — they appear as earlier ones close)`);
|
|
338
328
|
}
|
|
@@ -341,7 +331,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
341
331
|
for (const request of envelope.openRequestsAwaitingMe ?? []) {
|
|
342
332
|
const key = `request:${request.agreementId}:${request.ts}`;
|
|
343
333
|
if (deps.seen.has(key)) {
|
|
344
|
-
handledSoFar.add(request.
|
|
334
|
+
handledSoFar.add(request.receiptId);
|
|
345
335
|
continue;
|
|
346
336
|
}
|
|
347
337
|
deps.seen.mark(key);
|
|
@@ -349,14 +339,14 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
349
339
|
const handled = await attempt(`request ${request.agreementId}`, () => deps.onRequest(request), () => deps.seen.unmark(key));
|
|
350
340
|
if (handled) {
|
|
351
341
|
requestsTriaged++;
|
|
352
|
-
handledSoFar.add(request.
|
|
342
|
+
handledSoFar.add(request.receiptId);
|
|
353
343
|
}
|
|
354
344
|
}
|
|
355
345
|
else {
|
|
356
346
|
// No handler → still count as handled (ack) so an old host cannot wedge
|
|
357
347
|
// forever on request mail it does not understand.
|
|
358
348
|
requestsTriaged++;
|
|
359
|
-
handledSoFar.add(request.
|
|
349
|
+
handledSoFar.add(request.receiptId);
|
|
360
350
|
}
|
|
361
351
|
}
|
|
362
352
|
// Pending decisions are standing state, not deliveries: they keep appearing
|
|
@@ -373,15 +363,18 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
373
363
|
if (deps.seen.has(key))
|
|
374
364
|
continue;
|
|
375
365
|
deps.seen.mark(key);
|
|
376
|
-
const handled = await attempt(`pending proposal ${proposal.agreementId}`, () =>
|
|
377
|
-
kind: 'agreement',
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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));
|
|
385
378
|
if (handled)
|
|
386
379
|
resourceEvents++;
|
|
387
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) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { TaskUpdateState } from '@ziggs-ai/contracts';
|
|
1
2
|
import type { WriteSource } from '@ziggs-ai/api-client';
|
|
2
|
-
import { type Task
|
|
3
|
+
import { type Task } from '@ziggs-ai/api-client';
|
|
3
4
|
import type { CountTasksFilters, CreateTaskData } from '@ziggs-ai/api-client';
|
|
4
5
|
export declare class TaskService {
|
|
5
6
|
private creds;
|
|
@@ -7,10 +8,9 @@ export declare class TaskService {
|
|
|
7
8
|
acknowledgeControl(taskId: string, requestId: string, supported?: boolean): Promise<import("@ziggs-ai/api-client").TaskWriteConfirm>;
|
|
8
9
|
getTask(taskId: string): Promise<Task>;
|
|
9
10
|
getActiveTasksForAgent(agentId: string): Promise<Task[]>;
|
|
10
|
-
getActiveTasksForChat(chatId: string): Promise<Task[]>;
|
|
11
11
|
getSubtasks(parentTaskId: string): Promise<Task[]>;
|
|
12
12
|
spawnUnderAgreement(agreementId: string, taskData: Omit<CreateTaskData, 'agreementId'>): Promise<Task>;
|
|
13
|
-
updateState(taskId: string, state:
|
|
13
|
+
updateState(taskId: string, state: TaskUpdateState, result?: unknown, idempotencyKey?: string, sources?: WriteSource[]): Promise<{
|
|
14
14
|
ok: true;
|
|
15
15
|
taskId: string;
|
|
16
16
|
state: string;
|
|
@@ -22,6 +22,7 @@ export declare class TaskService {
|
|
|
22
22
|
agreement?: string;
|
|
23
23
|
next: import("@ziggs-ai/api-client").NextCall[];
|
|
24
24
|
workContext?: import("@ziggs-ai/api-client").WorkContext;
|
|
25
|
+
success: true;
|
|
25
26
|
agreementId?: string;
|
|
26
27
|
updatedAt?: string;
|
|
27
28
|
status?: string;
|