@ziggs-ai/agent-sdk 0.15.0 → 0.17.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 +10 -0
- package/README.md +110 -88
- package/dist/{AgentHost.d.ts → Agent.d.ts} +66 -27
- package/dist/{AgentHost.js → Agent.js} +221 -119
- package/dist/adapters/AnthropicAdapter.d.ts +1 -1
- package/dist/adapters/AnthropicAdapter.js +1 -1
- package/dist/{agent/ClaudeSdkAgent.d.ts → brain/ClaudeBrain.d.ts} +23 -11
- package/dist/{agent/ClaudeSdkAgent.js → brain/ClaudeBrain.js} +145 -95
- package/dist/{agent/Agent.d.ts → brain/ZiggsBrain.d.ts} +23 -20
- package/dist/{agent/Agent.js → brain/ZiggsBrain.js} +12 -11
- package/dist/{agent → brain}/claudeCognition.d.ts +1 -1
- package/dist/{agent → brain}/claudeCognition.js +1 -1
- package/dist/{agent → brain}/claudeSdkProtocol.d.ts +2 -2
- package/dist/{agent → brain}/claudeSdkProtocol.js +2 -2
- package/dist/{agent → brain}/claudeSdkTools.d.ts +2 -1
- package/dist/{agent → brain}/claudeSdkTools.js +9 -65
- package/dist/{agent → brain}/hostedAgentConnection.d.ts +1 -1
- package/dist/{agent → brain}/hostedAgentConnection.js +3 -3
- package/dist/brain/index.d.ts +6 -6
- package/dist/brain/index.js +2 -2
- package/dist/brain/onBehalfAgreement.d.ts +51 -0
- package/dist/brain/onBehalfAgreement.js +99 -0
- package/dist/{agent → brain}/toolManagerToSdkMcp.js +10 -1
- package/dist/defineAgent.js +4 -4
- package/dist/formatters/HistoryFormatter.js +2 -1
- package/dist/index.d.ts +30 -31
- package/dist/index.js +24 -25
- package/dist/{adapters/McpAdapter.d.ts → mcp/createMcpAgent.d.ts} +16 -15
- package/dist/{adapters/McpAdapter.js → mcp/createMcpAgent.js} +54 -40
- package/dist/memory/MemoryStore.d.ts +18 -1
- package/dist/memory/MemoryStore.js +43 -0
- package/dist/{server → platform}/ConnectionPool.d.ts +9 -3
- package/dist/{server → platform}/ConnectionPool.js +16 -4
- package/dist/{server → platform}/EventQueue.d.ts +1 -1
- package/dist/{server → platform}/StateTimers.d.ts +1 -1
- package/dist/{server → platform}/StateTimers.js +1 -1
- package/dist/{server → platform}/ZiggsEffectHandler.d.ts +4 -4
- package/dist/{server → platform}/ZiggsEffectHandler.js +2 -2
- package/dist/{server → platform}/agreements/AgreementService.d.ts +10 -1
- package/dist/{server → platform}/agreements/AgreementService.js +9 -1
- package/dist/{server → platform}/inboxLoop.d.ts +34 -1
- package/dist/{server → platform}/inboxLoop.js +122 -14
- package/dist/{server → platform}/polls/PollKind.d.ts +4 -7
- package/dist/{server → platform}/polls/PollKind.js +3 -6
- package/dist/{server → platform}/polls/PollRunner.d.ts +5 -6
- package/dist/{server → platform}/polls/PollRunner.js +24 -12
- package/dist/platform/polls/declaredPollRunner.d.ts +4 -0
- package/dist/platform/polls/declaredPollRunner.js +29 -0
- package/dist/{server → platform}/tools/index.d.ts +1 -0
- package/dist/{server → platform}/tools/index.js +1 -0
- package/dist/{server → platform}/tools/tier1/protocolRunner.js +33 -27
- package/dist/{server → platform}/tools/tier1/protocolTools.js +2 -7
- package/dist/{server → platform}/tools/tier2/mcpConnectionTools.js +6 -3
- package/dist/platform/tools/tier2/taskTools.d.ts +3 -0
- package/dist/platform/tools/tier2/taskTools.js +6 -0
- package/dist/runtime/bootstrapCtx.d.ts +1 -1
- package/dist/runtime/bootstrapCtx.js +1 -1
- package/dist/runtime/buildOutcome.d.ts +0 -8
- package/dist/runtime/buildOutcome.js +20 -6
- package/dist/runtime/kickoff.js +1 -1
- package/dist/runtime/runTurn.d.ts +1 -1
- package/dist/runtime/runTurn.js +18 -18
- package/dist/runtime/validateWorkflow.js +1 -1
- package/dist/runtime/wakeOutcome.d.ts +27 -0
- package/dist/runtime/wakeOutcome.js +43 -0
- package/dist/skills/parseSkill.d.ts +7 -2
- package/dist/skills/parseSkill.js +22 -5
- package/dist/skills/skillReadReference.d.ts +6 -0
- package/dist/skills/skillReadReference.js +27 -0
- package/dist/tools/needs.js +2 -2
- package/dist/tools/recordReport.d.ts +5 -0
- package/dist/tools/recordReport.js +5 -0
- package/dist/tools/toolCreds.d.ts +1 -1
- package/dist/tools/toolCreds.js +1 -1
- package/dist/types.d.ts +3 -3
- package/package.json +2 -2
- package/dist/server/createHealthServer.d.ts +0 -7
- package/dist/server/createHealthServer.js +0 -75
- package/dist/server/runLauncher.d.ts +0 -22
- package/dist/server/runLauncher.js +0 -160
- /package/dist/{agent → brain}/toolManagerToSdkMcp.d.ts +0 -0
- /package/dist/{server → platform}/EventQueue.js +0 -0
- /package/dist/{server → platform}/OutboxBuffer.d.ts +0 -0
- /package/dist/{server → platform}/OutboxBuffer.js +0 -0
- /package/dist/{server → platform}/SeenMessages.d.ts +0 -0
- /package/dist/{server → platform}/SeenMessages.js +0 -0
- /package/dist/{server → platform}/proactive/ProactiveTrigger.d.ts +0 -0
- /package/dist/{server → platform}/proactive/ProactiveTrigger.js +0 -0
- /package/dist/{server → platform}/requestMatch.d.ts +0 -0
- /package/dist/{server → platform}/requestMatch.js +0 -0
- /package/dist/{server → platform}/tasks/TaskService.d.ts +0 -0
- /package/dist/{server → platform}/tasks/TaskService.js +0 -0
- /package/dist/{server → platform}/tasks/index.d.ts +0 -0
- /package/dist/{server → platform}/tasks/index.js +0 -0
- /package/dist/{server → platform}/telemetryIngest.d.ts +0 -0
- /package/dist/{server → platform}/telemetryIngest.js +0 -0
- /package/dist/{server → platform}/tools/tier1/protocolRunner.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier1/protocolTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/artifactTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/artifactTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/chatTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/chatTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/connectionTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/connectionTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/contextTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/contextTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/discoveryTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/discoveryTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/grantsTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/grantsTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/introductionTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/introductionTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/linkTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/linkTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/marketplaceTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/marketplaceTools.js +0 -0
- /package/dist/{server → platform}/tools/tier2/mcpConnectionTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/paymentTools.d.ts +0 -0
- /package/dist/{server → platform}/tools/tier2/paymentTools.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ConnectionPool — the agents this launcher process hosts.
|
|
3
3
|
*
|
|
4
|
-
* Holds N `
|
|
4
|
+
* Holds N `AgentOptions` configs and connects every one of them at boot
|
|
5
5
|
*. Each host long-polls its own inbox for the life of the process —
|
|
6
6
|
* there is no agent socket. There is no subset, no cap and no lifecycle policy:
|
|
7
7
|
* a write releasing that poll is the only signal anything needs.
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* answer was "none of them".
|
|
15
15
|
*/
|
|
16
16
|
import { ConnectionManager } from '@ziggs-ai/api-client';
|
|
17
|
-
import {
|
|
17
|
+
import { createAgent } from '../Agent.js';
|
|
18
18
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
19
19
|
export class ConnectionPool {
|
|
20
20
|
manager = new ConnectionManager();
|
|
@@ -48,7 +48,7 @@ export class ConnectionPool {
|
|
|
48
48
|
const cfg = this.configs.get(agentId);
|
|
49
49
|
const originalTap = cfg.effectTap;
|
|
50
50
|
const pool = this;
|
|
51
|
-
const host =
|
|
51
|
+
const host = createAgent({
|
|
52
52
|
...cfg,
|
|
53
53
|
effectTap(effect, response) {
|
|
54
54
|
if (effect.kind === 'send-message') {
|
|
@@ -72,7 +72,11 @@ export class ConnectionPool {
|
|
|
72
72
|
}
|
|
73
73
|
/** Connect every registered agent. Reports the ones that could not come up. */
|
|
74
74
|
async connectAll() {
|
|
75
|
-
|
|
75
|
+
await this.manager.connectAll();
|
|
76
|
+
const connected = this.listActive().length;
|
|
77
|
+
if (connected !== this.size) {
|
|
78
|
+
throw new Error(`Only ${connected}/${this.size} agents started; process must restart`);
|
|
79
|
+
}
|
|
76
80
|
}
|
|
77
81
|
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
78
82
|
async startAgent(agentId) {
|
|
@@ -181,6 +185,14 @@ export class ConnectionPool {
|
|
|
181
185
|
query(filter) { return this.manager.query(filter); }
|
|
182
186
|
list() { return this.manager.list(); }
|
|
183
187
|
listActive() { return this.manager.listActive(); }
|
|
188
|
+
/**
|
|
189
|
+
* Inbox liveness for every live agent in this process. The runner's health
|
|
190
|
+
* route answers from this rather than from "the HTTP server replied", which
|
|
191
|
+
* a wedged process does happily while none of its agents read anything.
|
|
192
|
+
*/
|
|
193
|
+
liveness() {
|
|
194
|
+
return [...this.hosts.values()].map((host) => host.inboxLiveness());
|
|
195
|
+
}
|
|
184
196
|
get size() { return this.manager.size; }
|
|
185
197
|
getMeta(id) { return this.manager.getMeta(id); }
|
|
186
198
|
}
|
|
@@ -7,7 +7,7 @@ export interface EventQueueBusyLane {
|
|
|
7
7
|
queued: number;
|
|
8
8
|
processing: boolean;
|
|
9
9
|
}
|
|
10
|
-
/** Outcome of `waitForIdle` / `
|
|
10
|
+
/** Outcome of `waitForIdle` / `Agent.drain`. Timeout is data, not a throw. */
|
|
11
11
|
export interface WaitForIdleResult {
|
|
12
12
|
idle: boolean;
|
|
13
13
|
timedOut: boolean;
|
|
@@ -6,7 +6,7 @@ export interface StateTimeoutEvent {
|
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
8
|
* Per-lane liveness timers for parked states declaring timeoutMs. Owned by
|
|
9
|
-
*
|
|
9
|
+
* Agent. A firing timer enqueues a state-timeout frame on the SAME lane
|
|
10
10
|
* (caller-provided enqueue → EventQueue), so it serializes behind any real
|
|
11
11
|
* event and can never race one. Epochs make stale frames identifiable: a
|
|
12
12
|
* queued timeout frame whose epoch is not the lane's currently-armed epoch was
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runtimeLog } from '../shared/runtimeLog.js';
|
|
2
2
|
/**
|
|
3
3
|
* Per-lane liveness timers for parked states declaring timeoutMs. Owned by
|
|
4
|
-
*
|
|
4
|
+
* Agent. A firing timer enqueues a state-timeout frame on the SAME lane
|
|
5
5
|
* (caller-provided enqueue → EventQueue), so it serializes behind any real
|
|
6
6
|
* event and can never race one. Epochs make stale frames identifiable: a
|
|
7
7
|
* queued timeout frame whose epoch is not the lane's currently-armed epoch was
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Ziggs-flavored EffectHandler.
|
|
3
3
|
*
|
|
4
|
-
* Translates the
|
|
4
|
+
* Translates the brain's Effect requests into concrete backend calls:
|
|
5
5
|
* reads go through the grant-fenced ContextReadClient snapshot, writes through
|
|
6
6
|
* artifactsClient / taskService / agreementService / messageSender, and LLM
|
|
7
|
-
* turns through the adapter. This is the only place where
|
|
7
|
+
* turns through the adapter. This is the only place where brain intent meets
|
|
8
8
|
* backend reality.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Belongs to the SDK body. The operator runner owns process startup and
|
|
11
|
+
* health serving; it does not perform the brain’s effects.
|
|
12
12
|
*/
|
|
13
13
|
import type { Effect, EffectHandler, LlmMessage, LlmResponse, LlmToolSchema, ToolCallResult } from '../types.js';
|
|
14
14
|
/** Minimal LLM adapter contract — OpenAIAdapter satisfies this. */
|
|
@@ -115,7 +115,7 @@ export async function executeToolWithSideEffects(deps, eff, effect, roster) {
|
|
|
115
115
|
const argsPreview = previewToolArgs(args);
|
|
116
116
|
// #6 — breadcrumbs are best-effort. Awaiting them bracketed every
|
|
117
117
|
// tool call; a slow/wedged artifacts backend stalled the whole wake. Match
|
|
118
|
-
//
|
|
118
|
+
// ClaudeBrain thought writes (fire-and-forget).
|
|
119
119
|
void emitToolOperationArtifact(deps.artifactsClient, effect.sessionId, 'started', {
|
|
120
120
|
state: 'started',
|
|
121
121
|
tool: effect.name,
|
|
@@ -230,7 +230,7 @@ export function createZiggsEffectHandler(deps) {
|
|
|
230
230
|
case 'read-context': {
|
|
231
231
|
// Grant-fenced aggregate read: the backend composes the ContextSnapshot
|
|
232
232
|
// (GET /context/snapshot) through the same access + temporal fence as
|
|
233
|
-
// the per-type reads. Every producer —
|
|
233
|
+
// the per-type reads. Every producer — ClaudeBrain.tick, runTurn,
|
|
234
234
|
// and the on-demand context_snapshot tool — routes through here.
|
|
235
235
|
const snap = await buildSnapshot(deps, effect.sessionId, deps.agentId, {
|
|
236
236
|
limit: effect.opts?.maxMessages,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Agreement, type ClaimedKind, type ClaimOptions, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type DelegateAgreementData, type CounterAgreementData, type CreateLinkBody, type CreateLinkResult, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType } from '@ziggs-ai/api-client';
|
|
1
|
+
import { type Agreement, type ClaimedKind, type ClaimOptions, type ProposeAgreementData, type ProposeDirectInput, type ProposeBroadcastInput, type PublishOfferPayload, type UnifiedProposeInput, type ProposeShape, type DelegateAgreementData, type CounterAgreementData, type CreateLinkBody, type CreateLinkResult, type ListAgreementsFilters, type GetMyAgreementsFilters, type ChatLinkType, type ArtifactLinkType } from '@ziggs-ai/api-client';
|
|
2
2
|
export declare class AgreementService {
|
|
3
3
|
private creds;
|
|
4
4
|
constructor(operatorKey: string, agentId: string);
|
|
@@ -12,6 +12,15 @@ export declare class AgreementService {
|
|
|
12
12
|
proposeBroadcast(payload: ProposeBroadcastInput): Promise<Agreement>;
|
|
13
13
|
/** @deprecated Alias for {@link proposeDirect}. */
|
|
14
14
|
propose(payload: ProposeAgreementData): Promise<Agreement>;
|
|
15
|
+
/**
|
|
16
|
+
* One propose grammar (direct / request / standing offer). Fleet pricing
|
|
17
|
+
* and the wake idempotency key stay on the caller — this is the shared
|
|
18
|
+
* routing, not a second copy of it.
|
|
19
|
+
*/
|
|
20
|
+
proposeUnified(input: UnifiedProposeInput): Promise<{
|
|
21
|
+
agreement: Agreement;
|
|
22
|
+
shape: ProposeShape;
|
|
23
|
+
}>;
|
|
15
24
|
/**
|
|
16
25
|
* Seller-broadcast marketplace offer. `payload.audience` sets the `payer`
|
|
17
26
|
* sentinel: 'everyone' (default, public) or 'org' (your active org only).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { proposeAgreement, proposeBroadcast as proposeBroadcastApi, delegateAgreement, respondToAgreement, counterAgreement, claimOpenAgreement, getAgreementStatus, listAgreements, getMyAgreements, getAgreement, publishOffer as publishOfferApi, createLink, revokeAgreement, fulfillAgreement, linkAgreementToChat, getChatsForAgreement, linkArtifactToAgreement, getArtifactsForAgreement, } from '@ziggs-ai/api-client';
|
|
1
|
+
import { proposeAgreement, proposeBroadcast as proposeBroadcastApi, delegateAgreement, respondToAgreement, counterAgreement, claimOpenAgreement, proposeUnified, getAgreementStatus, listAgreements, getMyAgreements, getAgreement, publishOffer as publishOfferApi, createLink, revokeAgreement, fulfillAgreement, linkAgreementToChat, getChatsForAgreement, linkArtifactToAgreement, getArtifactsForAgreement, } from '@ziggs-ai/api-client';
|
|
2
2
|
export class AgreementService {
|
|
3
3
|
creds;
|
|
4
4
|
constructor(operatorKey, agentId) {
|
|
@@ -24,6 +24,14 @@ export class AgreementService {
|
|
|
24
24
|
propose(payload) {
|
|
25
25
|
return proposeAgreement(payload, this.creds);
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* One propose grammar (direct / request / standing offer). Fleet pricing
|
|
29
|
+
* and the wake idempotency key stay on the caller — this is the shared
|
|
30
|
+
* routing, not a second copy of it.
|
|
31
|
+
*/
|
|
32
|
+
proposeUnified(input) {
|
|
33
|
+
return proposeUnified(input, this.creds);
|
|
34
|
+
}
|
|
27
35
|
/**
|
|
28
36
|
* Seller-broadcast marketplace offer. `payload.audience` sets the `payer`
|
|
29
37
|
* sentinel: 'everyone' (default, public) or 'org' (your active org only).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type InboxRequestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
|
|
1
|
+
import { type InboxChatNews, type InboxRequestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
|
|
2
2
|
import { SeenMessages } from './SeenMessages.js';
|
|
3
3
|
/**
|
|
4
4
|
* Per-agent inbox long-poll loop (delivery law, phase 4). This is the ONLY way
|
|
@@ -53,6 +53,19 @@ export interface InboxPassResult {
|
|
|
53
53
|
*/
|
|
54
54
|
failures: number;
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Store publish waits on a `probe_*` chat reply, not inbox ack. Act those
|
|
58
|
+
* rooms before leftover mail so a standing host answers the gate even when
|
|
59
|
+
* the envelope also carries older chats.
|
|
60
|
+
*/
|
|
61
|
+
export declare function chatsInWakeOrder(chats: InboxChatNews[]): InboxChatNews[];
|
|
62
|
+
/**
|
|
63
|
+
* Store publish waits on a chat reply, not inbox ack. The ordinary pass can
|
|
64
|
+
* already be inside leftover mail when that room appears, so a side read
|
|
65
|
+
* answers `probe_*` chats without waiting for the current pass to finish.
|
|
66
|
+
* It never acks — the main pass still owns the watermark.
|
|
67
|
+
*/
|
|
68
|
+
export declare function answerPublishProbes(deps: InboxLoopDeps): Promise<number>;
|
|
56
69
|
/**
|
|
57
70
|
* One long-poll pass: hold up to `waitSeconds` for new inbox content, then read
|
|
58
71
|
* the deltas, deliver them into the host, and ack. Returns what it delivered.
|
|
@@ -67,7 +80,26 @@ export interface InboxLoopHandle {
|
|
|
67
80
|
stop(): Promise<void>;
|
|
68
81
|
/** True while the loop is running. */
|
|
69
82
|
readonly running: boolean;
|
|
83
|
+
/**
|
|
84
|
+
* When this loop last completed a pass against the server — the one true
|
|
85
|
+
* liveness signal for an agent, and the same fact the store shows as online.
|
|
86
|
+
* Null until the first pass returns.
|
|
87
|
+
*/
|
|
88
|
+
readonly lastPassAt: number | null;
|
|
89
|
+
/**
|
|
90
|
+
* When this loop last STARTED an iteration, successful or not. Separate from
|
|
91
|
+
* `lastPassAt` on purpose: a backend outage stops passes while the loop is
|
|
92
|
+
* perfectly healthy, so a restart decision reads this one and a staleness
|
|
93
|
+
* report reads the other. `running` says the loop exists; this says it turns.
|
|
94
|
+
*/
|
|
95
|
+
readonly lastAttemptAt: number | null;
|
|
70
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* How many consecutive passes have refused to advance the watermark.
|
|
99
|
+
* A capped envelope used to reset the counter (zero handover failures),
|
|
100
|
+
* so `forceAck` never fired on the one hold that lasts forever.
|
|
101
|
+
*/
|
|
102
|
+
export declare function heldPassAfter(prev: number, pass: Pick<InboxPassResult, 'acked' | 'failures' | 'truncated'>): number;
|
|
71
103
|
/**
|
|
72
104
|
* Run {@link pollInboxOnce} forever (until stopped). Each iteration long-polls
|
|
73
105
|
* for `waitSeconds`; a passing envelope returns instantly, an empty one holds.
|
|
@@ -82,4 +114,5 @@ export interface InboxLoopHandle {
|
|
|
82
114
|
*/
|
|
83
115
|
export declare function runInboxLoop(deps: InboxLoopDeps, opts?: {
|
|
84
116
|
waitSeconds?: number;
|
|
117
|
+
probePollMs?: number;
|
|
85
118
|
}): InboxLoopHandle;
|
|
@@ -8,6 +8,62 @@ function log(label, msg) {
|
|
|
8
8
|
function warn(label, msg) {
|
|
9
9
|
runtimeLog.warn(label, msg);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Store publish waits on a `probe_*` chat reply, not inbox ack. Act those
|
|
13
|
+
* rooms before leftover mail so a standing host answers the gate even when
|
|
14
|
+
* the envelope also carries older chats.
|
|
15
|
+
*/
|
|
16
|
+
export function chatsInWakeOrder(chats) {
|
|
17
|
+
return [...chats].sort((a, b) => {
|
|
18
|
+
const ap = isPublishProbeChat(a.chatId) ? 0 : 1;
|
|
19
|
+
const bp = isPublishProbeChat(b.chatId) ? 0 : 1;
|
|
20
|
+
if (ap !== bp)
|
|
21
|
+
return ap - bp;
|
|
22
|
+
return a.latestAt < b.latestAt ? 1 : -1;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function isPublishProbeChat(chatId) {
|
|
26
|
+
return chatId.startsWith('probe_');
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Store publish waits on a chat reply, not inbox ack. The ordinary pass can
|
|
30
|
+
* already be inside leftover mail when that room appears, so a side read
|
|
31
|
+
* answers `probe_*` chats without waiting for the current pass to finish.
|
|
32
|
+
* It never acks — the main pass still owns the watermark.
|
|
33
|
+
*/
|
|
34
|
+
export async function answerPublishProbes(deps) {
|
|
35
|
+
const inbox = new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
36
|
+
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
37
|
+
const envelope = await inbox.getInbox({});
|
|
38
|
+
const probes = (envelope.chats ?? []).filter((c) => isPublishProbeChat(c.chatId));
|
|
39
|
+
if (probes.length === 0)
|
|
40
|
+
return 0;
|
|
41
|
+
const mine = (d) => d.assigneeId === deps.ownAgentId;
|
|
42
|
+
const earliestByChat = new Map();
|
|
43
|
+
for (const d of envelope.deliveries) {
|
|
44
|
+
if (!d.chatId || !mine(d) || !isPublishProbeChat(d.chatId))
|
|
45
|
+
continue;
|
|
46
|
+
const seen = earliestByChat.get(d.chatId);
|
|
47
|
+
if (!seen || d.ts < seen)
|
|
48
|
+
earliestByChat.set(d.chatId, d.ts);
|
|
49
|
+
}
|
|
50
|
+
const attempt = async (what, handover) => {
|
|
51
|
+
try {
|
|
52
|
+
await handover();
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
warn(deps.label ?? 'inboxLoop', `publish-probe ${what} was not handled: ${err?.message ?? err}`);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
let delivered = 0;
|
|
61
|
+
for (const news of probes) {
|
|
62
|
+
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
63
|
+
delivered += await deliverMessagesForChat(deps, news, earliest, reader, attempt);
|
|
64
|
+
}
|
|
65
|
+
return delivered;
|
|
66
|
+
}
|
|
11
67
|
/**
|
|
12
68
|
* Standing proposals share the merged inbox visibility model with deliveries,
|
|
13
69
|
* but older envelopes predate an explicit assignee stamp. Prefer the server's
|
|
@@ -116,9 +172,9 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
116
172
|
}
|
|
117
173
|
};
|
|
118
174
|
if (envelope.deliveriesCapped) {
|
|
119
|
-
//
|
|
120
|
-
// tail stays
|
|
121
|
-
//
|
|
175
|
+
// The listed window is the oldest CAP. Acking it slides the cursor; the
|
|
176
|
+
// newer tail stays past ackTo and arrives on the next pass. Holding here
|
|
177
|
+
// was the wedge: the same over-cap window came back forever.
|
|
122
178
|
warn(label, 'inbox deliveriesCapped (more mail than one envelope carries — the tail follows on the next pass)');
|
|
123
179
|
}
|
|
124
180
|
// The single-assign law: a row is MINE to act on iff it carries my stamp.
|
|
@@ -136,7 +192,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
136
192
|
if (!seen || d.ts < seen)
|
|
137
193
|
earliestByChat.set(d.chatId, d.ts);
|
|
138
194
|
}
|
|
139
|
-
for (const news of envelope.chats) {
|
|
195
|
+
for (const news of chatsInWakeOrder(envelope.chats ?? [])) {
|
|
140
196
|
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
141
197
|
const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt);
|
|
142
198
|
messagesDelivered += n;
|
|
@@ -195,7 +251,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
195
251
|
}
|
|
196
252
|
// request channel. Triage is the host's job (exact string compare);
|
|
197
253
|
// a miss must still count as handled so the watermark can advance with 0 tokens.
|
|
198
|
-
for (const request of envelope.
|
|
254
|
+
for (const request of envelope.openRequestsAwaitingMe ?? []) {
|
|
199
255
|
const key = `request:${request.agreementId}:${request.ts}`;
|
|
200
256
|
if (deps.seen.has(key))
|
|
201
257
|
continue;
|
|
@@ -245,12 +301,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
245
301
|
// this window has failed too many times in a row and is now poison, in which
|
|
246
302
|
// case it is dropped loudly rather than wedging the agent forever.
|
|
247
303
|
let acked = false;
|
|
248
|
-
|
|
249
|
-
// acking would refuse or bury them. Hold until a later uncapped pass.
|
|
250
|
-
if (envelope.deliveriesCapped && envelope.ackTo) {
|
|
251
|
-
warn(label, `holding the watermark at ${envelope.ackTo} — deliveriesCapped; ack would not cover the unlisted tail`);
|
|
252
|
-
}
|
|
253
|
-
else if (envelope.ackTo && (failures === 0 || opts.forceAck)) {
|
|
304
|
+
if (envelope.ackTo && (failures === 0 || opts.forceAck)) {
|
|
254
305
|
if (failures > 0) {
|
|
255
306
|
runtimeLog.error(label, `acking a window that failed ${failures} handover(s) — giving up on redelivery to keep the ` +
|
|
256
307
|
`agent alive; this mail is lost (ackTo=${envelope.ackTo})`);
|
|
@@ -261,7 +312,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
261
312
|
const handledResourceIds = [
|
|
262
313
|
...new Set([
|
|
263
314
|
...envelope.deliveries.filter(mine).map((d) => d.resourceId),
|
|
264
|
-
...(envelope.
|
|
315
|
+
...(envelope.openRequestsAwaitingMe ?? []).map((q) => q.agreementId),
|
|
265
316
|
]),
|
|
266
317
|
].filter((id) => typeof id === 'string' && id.length > 0);
|
|
267
318
|
await inbox.ack(envelope.ackTo, { handledResourceIds });
|
|
@@ -289,6 +340,16 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
289
340
|
* some defaults ship with. Lower it if your deployment sits behind a shorter one.
|
|
290
341
|
*/
|
|
291
342
|
const DEFAULT_WAIT_SECONDS = 110;
|
|
343
|
+
/**
|
|
344
|
+
* Side-poll interval for store-publish `probe_*` rooms.
|
|
345
|
+
*
|
|
346
|
+
* Default **off**. A second GET /inbox beside the main long-poll burned the
|
|
347
|
+
* shared poll ceiling: every hosted agent side-polled, probes got 429s, and
|
|
348
|
+
* Register saw ALB HTML 504s. Main pass + probe-first ordering + pinned
|
|
349
|
+
* `probe_*` deliveries are enough; set `probePollMs` only when you knowingly
|
|
350
|
+
* need the extra loop.
|
|
351
|
+
*/
|
|
352
|
+
const DEFAULT_PROBE_POLL_MS = 0;
|
|
292
353
|
const ERROR_BACKOFF_BASE_MS = 1_000;
|
|
293
354
|
const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
294
355
|
/**
|
|
@@ -299,6 +360,14 @@ const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
|
299
360
|
* rather than an unbounded loop.
|
|
300
361
|
*/
|
|
301
362
|
const MAX_HELD_PASSES = 3;
|
|
363
|
+
/**
|
|
364
|
+
* How many consecutive passes have refused to advance the watermark.
|
|
365
|
+
* A capped envelope used to reset the counter (zero handover failures),
|
|
366
|
+
* so `forceAck` never fired on the one hold that lasts forever.
|
|
367
|
+
*/
|
|
368
|
+
export function heldPassAfter(prev, pass) {
|
|
369
|
+
return !pass.acked && (pass.failures > 0 || pass.truncated) ? prev + 1 : 0;
|
|
370
|
+
}
|
|
302
371
|
function sleep(ms) {
|
|
303
372
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
304
373
|
}
|
|
@@ -317,21 +386,28 @@ function sleep(ms) {
|
|
|
317
386
|
export function runInboxLoop(deps, opts = {}) {
|
|
318
387
|
const label = deps.label ?? 'inboxLoop';
|
|
319
388
|
const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
389
|
+
const probePollMs = opts.probePollMs ?? DEFAULT_PROBE_POLL_MS;
|
|
320
390
|
let running = true;
|
|
321
391
|
let consecutiveErrors = 0;
|
|
322
392
|
let heldPasses = 0;
|
|
393
|
+
let lastPassAt = null;
|
|
394
|
+
let lastAttemptAt = null;
|
|
323
395
|
let first = true;
|
|
324
|
-
const
|
|
396
|
+
const main = (async () => {
|
|
325
397
|
while (running) {
|
|
326
398
|
try {
|
|
399
|
+
lastAttemptAt = Date.now();
|
|
327
400
|
const pass = await pollInboxOnce(deps, first ? 0 : waitSeconds, {
|
|
328
401
|
forceAck: heldPasses >= MAX_HELD_PASSES,
|
|
329
402
|
});
|
|
403
|
+
lastPassAt = Date.now();
|
|
330
404
|
first = false;
|
|
331
405
|
consecutiveErrors = 0;
|
|
332
406
|
// A held pass means the same window comes back; count how many times we
|
|
333
407
|
// have refused it so a permanently unhandleable frame cannot spin here.
|
|
334
|
-
|
|
408
|
+
// Capped holds used to reset this: failures === 0, so the poison
|
|
409
|
+
// escape never climbed. Count any pass that refused to advance.
|
|
410
|
+
heldPasses = heldPassAfter(heldPasses, pass);
|
|
335
411
|
}
|
|
336
412
|
catch (err) {
|
|
337
413
|
if (!running)
|
|
@@ -356,10 +432,42 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
356
432
|
}
|
|
357
433
|
}
|
|
358
434
|
})();
|
|
435
|
+
const probes = (async () => {
|
|
436
|
+
if (probePollMs <= 0)
|
|
437
|
+
return;
|
|
438
|
+
while (running) {
|
|
439
|
+
const slice = 50;
|
|
440
|
+
let left = probePollMs;
|
|
441
|
+
while (left > 0 && running) {
|
|
442
|
+
const step = Math.min(slice, left);
|
|
443
|
+
await sleep(step);
|
|
444
|
+
left -= step;
|
|
445
|
+
}
|
|
446
|
+
if (!running)
|
|
447
|
+
break;
|
|
448
|
+
try {
|
|
449
|
+
const n = await answerPublishProbes(deps);
|
|
450
|
+
if (n > 0)
|
|
451
|
+
log(label, `publish-probe side poll delivered ${n} message(s)`);
|
|
452
|
+
}
|
|
453
|
+
catch (err) {
|
|
454
|
+
if (!running)
|
|
455
|
+
break;
|
|
456
|
+
warn(label, `publish-probe side poll failed: ${err?.message ?? err}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
})();
|
|
460
|
+
const done = Promise.all([main, probes]);
|
|
359
461
|
return {
|
|
360
462
|
get running() {
|
|
361
463
|
return running;
|
|
362
464
|
},
|
|
465
|
+
get lastPassAt() {
|
|
466
|
+
return lastPassAt;
|
|
467
|
+
},
|
|
468
|
+
get lastAttemptAt() {
|
|
469
|
+
return lastAttemptAt;
|
|
470
|
+
},
|
|
363
471
|
async stop() {
|
|
364
472
|
running = false;
|
|
365
473
|
await done.catch(() => { });
|
|
@@ -5,12 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* polls: [{ kind: 'sente-email', identityId: 'idt_...' }]
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* with its own interval, backoff and backpressure, instead of every host
|
|
12
|
-
* hand-rolling a timer inside its message loop. Declaration travels with the
|
|
13
|
-
* agent; execution does not.
|
|
8
|
+
* The declaration and its PollRunner travel with the Agent. connectAsync starts
|
|
9
|
+
* both inbox delivery and external polls; disconnect stops both. The process
|
|
10
|
+
* runner does not own a second copy of the polling lifecycle.
|
|
14
11
|
*
|
|
15
12
|
* A poll answers ONE question: is there work for this agent right now. It never
|
|
16
13
|
* returns the work itself. The agent reads that for itself once woken, through
|
|
@@ -26,7 +23,7 @@
|
|
|
26
23
|
* Kind-specific config, verbatim from the agent's manifest.
|
|
27
24
|
*
|
|
28
25
|
* `chatId` is not kind-specific and is not optional in practice: a wake has to
|
|
29
|
-
* resolve to a lane or
|
|
26
|
+
* resolve to a lane or Agent's session resolver returns null and drops it
|
|
30
27
|
* silently. A poll has no engagement behind it to derive one from, so the
|
|
31
28
|
* binding names the chat the agent thinks in — which also puts its triage
|
|
32
29
|
* somewhere a human can watch. PollRunner refuses to register a binding without
|
|
@@ -5,12 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* polls: [{ kind: 'sente-email', identityId: 'idt_...' }]
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* with its own interval, backoff and backpressure, instead of every host
|
|
12
|
-
* hand-rolling a timer inside its message loop. Declaration travels with the
|
|
13
|
-
* agent; execution does not.
|
|
8
|
+
* The declaration and its PollRunner travel with the Agent. connectAsync starts
|
|
9
|
+
* both inbox delivery and external polls; disconnect stops both. The process
|
|
10
|
+
* runner does not own a second copy of the polling lifecycle.
|
|
14
11
|
*
|
|
15
12
|
* A poll answers ONE question: is there work for this agent right now. It never
|
|
16
13
|
* returns the work itself. The agent reads that for itself once woken, through
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PollRunner — executes declared
|
|
2
|
+
* PollRunner — executes declared external wake sources for agents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* because none of it belongs to a host's message loop: a poll is per
|
|
8
|
-
* (agent, binding) and carries its own interval, backoff and backpressure.
|
|
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.
|
|
9
7
|
*
|
|
10
8
|
* One loop per (agent, binding), and each loop AWAITS its own wake before
|
|
11
9
|
* scheduling the next tick. That is the whole backpressure story: while an
|
|
@@ -73,6 +71,7 @@ export declare class PollRunner {
|
|
|
73
71
|
private readonly wakeWindowMs;
|
|
74
72
|
private readonly loops;
|
|
75
73
|
private running;
|
|
74
|
+
private generation;
|
|
76
75
|
constructor({ pool, registry, agents, operatorKey, defaultIntervalMs, maxBackoffMs, wakeTimeoutMs, maxWakesPerWindow, wakeWindowMs, label, }: PollRunnerOptions);
|
|
77
76
|
get size(): number;
|
|
78
77
|
start(): void;
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PollRunner — executes declared
|
|
2
|
+
* PollRunner — executes declared external wake sources for agents.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* because none of it belongs to a host's message loop: a poll is per
|
|
8
|
-
* (agent, binding) and carries its own interval, backoff and backpressure.
|
|
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.
|
|
9
7
|
*
|
|
10
8
|
* One loop per (agent, binding), and each loop AWAITS its own wake before
|
|
11
9
|
* scheduling the next tick. That is the whole backpressure story: while an
|
|
@@ -30,6 +28,7 @@ export class PollRunner {
|
|
|
30
28
|
wakeWindowMs;
|
|
31
29
|
loops = [];
|
|
32
30
|
running = false;
|
|
31
|
+
generation = 0;
|
|
33
32
|
constructor({ pool, registry, agents, operatorKey, defaultIntervalMs = 60_000, maxBackoffMs = 15 * 60_000, wakeTimeoutMs = Number(process.env.CLAUDE_SDK_TICK_TIMEOUT_MS ?? 300_000), maxWakesPerWindow = 20, wakeWindowMs = 60 * 60_000, label = 'PollRunner', }) {
|
|
34
33
|
this.pool = pool;
|
|
35
34
|
this.operatorKey = operatorKey;
|
|
@@ -65,6 +64,7 @@ export class PollRunner {
|
|
|
65
64
|
chatId: binding.chatId,
|
|
66
65
|
timer: null,
|
|
67
66
|
failures: 0,
|
|
67
|
+
inFlight: false,
|
|
68
68
|
wakes: 0,
|
|
69
69
|
windowStartedAt: Date.now(),
|
|
70
70
|
capped: false,
|
|
@@ -79,14 +79,18 @@ export class PollRunner {
|
|
|
79
79
|
if (this.running)
|
|
80
80
|
return;
|
|
81
81
|
this.running = true;
|
|
82
|
+
this.generation++;
|
|
82
83
|
if (!this.loops.length)
|
|
83
84
|
return;
|
|
84
|
-
for (const loop of this.loops)
|
|
85
|
-
|
|
85
|
+
for (const loop of this.loops) {
|
|
86
|
+
if (!loop.inFlight)
|
|
87
|
+
this.schedule(loop, 0);
|
|
88
|
+
}
|
|
86
89
|
runtimeLog.info(this.label, `started — ${this.loops.length} poll(s): ${this.loops.map((l) => `${l.agentId}:${l.kind.id}`).join(', ')}`);
|
|
87
90
|
}
|
|
88
91
|
stop() {
|
|
89
92
|
this.running = false;
|
|
93
|
+
this.generation++;
|
|
90
94
|
for (const loop of this.loops) {
|
|
91
95
|
if (loop.timer)
|
|
92
96
|
clearTimeout(loop.timer);
|
|
@@ -97,7 +101,14 @@ export class PollRunner {
|
|
|
97
101
|
schedule(loop, delayMs = this.nextDelay(loop)) {
|
|
98
102
|
if (!this.running)
|
|
99
103
|
return;
|
|
100
|
-
loop.timer = setTimeout(() =>
|
|
104
|
+
loop.timer = setTimeout(() => {
|
|
105
|
+
loop.timer = null;
|
|
106
|
+
loop.inFlight = true;
|
|
107
|
+
void this.tick(loop, this.generation).finally(() => {
|
|
108
|
+
loop.inFlight = false;
|
|
109
|
+
this.schedule(loop);
|
|
110
|
+
});
|
|
111
|
+
}, delayMs);
|
|
101
112
|
}
|
|
102
113
|
/** Interval while healthy; doubling up to the ceiling while failing. */
|
|
103
114
|
nextDelay(loop) {
|
|
@@ -105,7 +116,7 @@ export class PollRunner {
|
|
|
105
116
|
return loop.intervalMs;
|
|
106
117
|
return Math.min(loop.intervalMs * 2 ** loop.failures, this.maxBackoffMs);
|
|
107
118
|
}
|
|
108
|
-
async tick(loop) {
|
|
119
|
+
async tick(loop, generation = this.generation) {
|
|
109
120
|
if (!this.running)
|
|
110
121
|
return;
|
|
111
122
|
let result;
|
|
@@ -119,9 +130,11 @@ export class PollRunner {
|
|
|
119
130
|
catch (err) {
|
|
120
131
|
loop.failures += 1;
|
|
121
132
|
runtimeLog.error(this.label, `${loop.agentId}:${loop.kind.id} source failed (${loop.failures}x, next in ${this.nextDelay(loop)}ms): ${err.message}`);
|
|
122
|
-
this.schedule(loop);
|
|
123
133
|
return;
|
|
124
134
|
}
|
|
135
|
+
// A source read may finish after disconnect or a new connection.
|
|
136
|
+
if (!this.running || generation !== this.generation)
|
|
137
|
+
return;
|
|
125
138
|
if (result?.actionable && this.claimWake(loop)) {
|
|
126
139
|
const hint = result.hint ?? `New work from ${loop.kind.id}. Check it.`;
|
|
127
140
|
try {
|
|
@@ -140,7 +153,6 @@ export class PollRunner {
|
|
|
140
153
|
runtimeLog.error(this.label, `${loop.agentId} wake failed (${loop.failures}x, next in ${this.nextDelay(loop)}ms): ${err.message}`);
|
|
141
154
|
}
|
|
142
155
|
}
|
|
143
|
-
this.schedule(loop);
|
|
144
156
|
}
|
|
145
157
|
/**
|
|
146
158
|
* Take one wake from this binding's budget, or refuse.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentOptions } from '../../Agent.js';
|
|
2
|
+
import { PollRunner, type WakeTarget } from './PollRunner.js';
|
|
3
|
+
/** Validate before starting any work, so a declared wake source cannot vanish. */
|
|
4
|
+
export declare function createDeclaredPollRunner(options: AgentOptions, target: WakeTarget): PollRunner | null;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PollRegistry } from './PollKind.js';
|
|
2
|
+
import { PollRunner } from './PollRunner.js';
|
|
3
|
+
/** Validate before starting any work, so a declared wake source cannot vanish. */
|
|
4
|
+
export function createDeclaredPollRunner(options, target) {
|
|
5
|
+
const registry = new PollRegistry();
|
|
6
|
+
for (const kind of options.pollKinds ?? [])
|
|
7
|
+
registry.register(kind);
|
|
8
|
+
if (options.polls === undefined)
|
|
9
|
+
return null;
|
|
10
|
+
if (!Array.isArray(options.polls))
|
|
11
|
+
throw new Error('Agent: polls must be an array');
|
|
12
|
+
for (const binding of options.polls) {
|
|
13
|
+
if (!binding || !registry.get(binding.kind)) {
|
|
14
|
+
throw new Error(`Agent ${options.agentId}: unknown poll kind "${binding?.kind}"; declare its implementation in pollKinds`);
|
|
15
|
+
}
|
|
16
|
+
if (typeof binding.chatId !== 'string' || !binding.chatId.trim()) {
|
|
17
|
+
throw new Error(`Agent ${options.agentId}: poll "${binding.kind}" requires a chatId`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (!options.polls.length)
|
|
21
|
+
return null;
|
|
22
|
+
return new PollRunner({
|
|
23
|
+
pool: target,
|
|
24
|
+
registry,
|
|
25
|
+
agents: [{ agentId: options.agentId, polls: options.polls }],
|
|
26
|
+
operatorKey: options.operatorKey,
|
|
27
|
+
label: `${options.name || options.agentId}-polls`,
|
|
28
|
+
});
|
|
29
|
+
}
|