@ziggs-ai/agent-sdk 0.20.0 → 0.21.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/dist/Agent.d.ts +28 -7
- package/dist/Agent.js +176 -71
- package/dist/brain/ClaudeBrain.d.ts +11 -1
- package/dist/brain/ClaudeBrain.js +71 -18
- package/dist/brain/ZiggsBrain.d.ts +2 -0
- package/dist/brain/ZiggsBrain.js +1 -0
- package/dist/brain/claudeCognition.d.ts +1 -1
- package/dist/brain/claudeCognition.js +47 -7
- package/dist/brain/claudeSdkProtocol.d.ts +1 -1
- package/dist/brain/claudeSdkProtocol.js +1 -1
- package/dist/brain/claudeSdkTools.d.ts +3 -0
- package/dist/brain/claudeSdkTools.js +4 -2
- package/dist/brain/hireActivationNotice.d.ts +19 -0
- package/dist/brain/hireActivationNotice.js +79 -0
- package/dist/brain/standingOfferNotice.d.ts +15 -0
- package/dist/brain/standingOfferNotice.js +89 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/ingress/normalizeIncoming.d.ts +3 -0
- package/dist/ingress/normalizeIncoming.js +8 -0
- package/dist/mcp/createMcpAgent.d.ts +1 -1
- package/dist/mcp/createMcpAgent.js +1 -1
- package/dist/memory/MemoryStore.d.ts +18 -4
- package/dist/memory/MemoryStore.js +52 -12
- package/dist/platform/ConnectionPool.d.ts +9 -4
- package/dist/platform/ConnectionPool.js +16 -6
- package/dist/platform/StateTimers.d.ts +12 -6
- package/dist/platform/StateTimers.js +26 -6
- package/dist/platform/ZiggsEffectHandler.js +5 -1
- package/dist/platform/inboxLoop.d.ts +5 -7
- package/dist/platform/inboxLoop.js +36 -80
- package/dist/platform/polls/declaredPollRunner.d.ts +7 -1
- package/dist/platform/polls/declaredPollRunner.js +7 -9
- package/dist/platform/tools/index.d.ts +1 -0
- package/dist/platform/tools/index.js +1 -0
- package/dist/platform/tools/tier2/contextTools.d.ts +2 -0
- package/dist/platform/tools/tier2/contextTools.js +3 -1
- package/dist/platform/tools/tier2/planTools.d.ts +20 -0
- package/dist/platform/tools/tier2/planTools.js +64 -0
- package/dist/pricing/fleetDefaults.js +4 -9
- package/dist/pricing/fleetTierByAgentId.gen.d.ts +1 -1
- package/dist/pricing/fleetTierByAgentId.gen.js +2 -752
- package/dist/runtime/AgentMachine.d.ts +3 -0
- package/dist/runtime/AgentMachine.js +6 -1
- package/dist/runtime/runTurn.d.ts +1 -0
- package/dist/runtime/runTurn.js +19 -15
- package/dist/runtime/wakeEffects.d.ts +35 -0
- package/dist/runtime/wakeEffects.js +121 -0
- package/package.json +2 -2
- package/dist/pricing/fleetFreeTierA.gen.d.ts +0 -6
- package/dist/pricing/fleetFreeTierA.gen.js +0 -11
|
@@ -192,7 +192,7 @@ export declare function trimDescription(desc: string | undefined): string | unde
|
|
|
192
192
|
* await agent.connectAsync();
|
|
193
193
|
*
|
|
194
194
|
* process.on('SIGTERM', async () => {
|
|
195
|
-
* agent.disconnect();
|
|
195
|
+
* await agent.disconnect();
|
|
196
196
|
* await agent.drain();
|
|
197
197
|
* await closeMcp();
|
|
198
198
|
* });
|
|
@@ -333,7 +333,7 @@ function capTools(tools, agentId, maxTools, extraToolCount, allowedTools) {
|
|
|
333
333
|
* await agent.connectAsync();
|
|
334
334
|
*
|
|
335
335
|
* process.on('SIGTERM', async () => {
|
|
336
|
-
* agent.disconnect();
|
|
336
|
+
* await agent.disconnect();
|
|
337
337
|
* await agent.drain();
|
|
338
338
|
* await closeMcp();
|
|
339
339
|
* });
|
|
@@ -56,15 +56,29 @@ export declare class FileMemoryStore implements MemoryStore {
|
|
|
56
56
|
delete(key: string): Promise<void>;
|
|
57
57
|
}
|
|
58
58
|
/**
|
|
59
|
-
* Section a wake may read and write.
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
59
|
+
* Section a wake may read and write.
|
|
60
|
+
*
|
|
61
|
+
* Pick from the hire this wake is acting under — the same fact the
|
|
62
|
+
* server uses for acting-for: a pinned agreement, the delivery / event
|
|
63
|
+
* hire, or an `agrn-` lane. A chat-room wake with exactly one hire
|
|
64
|
+
* behind it lands in that hire's section. Zero hires, or more than one
|
|
65
|
+
* customer present, share the agent's own section — the "operator only"
|
|
66
|
+
* answer in memory form. Never pick from the chat id alone.
|
|
63
67
|
*/
|
|
64
68
|
export declare function memorySectionForWake(input: {
|
|
65
69
|
sessionId?: string | null;
|
|
66
70
|
activeAgreementId?: string | null;
|
|
71
|
+
/** Hire ids already on this wake (delivery / event / acting-for). */
|
|
72
|
+
wakeAgreementIds?: readonly (string | null | undefined)[];
|
|
67
73
|
}): string;
|
|
74
|
+
/** Distinct non-empty hire ids, first-seen order. */
|
|
75
|
+
export declare function uniqueHireIds(raw: readonly (string | null | undefined)[]): string[];
|
|
76
|
+
/**
|
|
77
|
+
* Hire ids the waking event already named. A chat-room message that
|
|
78
|
+
* carries the delivery's `agreementId` is how two customer rooms
|
|
79
|
+
* partition without a pinned ctx.
|
|
80
|
+
*/
|
|
81
|
+
export declare function hireIdsFromEvent(event: unknown): string[];
|
|
68
82
|
/**
|
|
69
83
|
* Present a store as if it only contained one section. Agent keys stay
|
|
70
84
|
* unchanged; get/put/list/delete all go through `section:`.
|
|
@@ -78,22 +78,62 @@ export class FileMemoryStore {
|
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
|
-
* Section a wake may read and write.
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
81
|
+
* Section a wake may read and write.
|
|
82
|
+
*
|
|
83
|
+
* Pick from the hire this wake is acting under — the same fact the
|
|
84
|
+
* server uses for acting-for: a pinned agreement, the delivery / event
|
|
85
|
+
* hire, or an `agrn-` lane. A chat-room wake with exactly one hire
|
|
86
|
+
* behind it lands in that hire's section. Zero hires, or more than one
|
|
87
|
+
* customer present, share the agent's own section — the "operator only"
|
|
88
|
+
* answer in memory form. Never pick from the chat id alone.
|
|
85
89
|
*/
|
|
86
90
|
export function memorySectionForWake(input) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return `hire:${fromLane}`;
|
|
91
|
+
const ids = uniqueHireIds([
|
|
92
|
+
input.activeAgreementId,
|
|
93
|
+
...(input.wakeAgreementIds ?? []),
|
|
94
|
+
agreementIdFromAgrnSession(input.sessionId),
|
|
95
|
+
]);
|
|
96
|
+
if (ids.length === 1)
|
|
97
|
+
return `hire:${ids[0]}`;
|
|
95
98
|
return 'agent';
|
|
96
99
|
}
|
|
100
|
+
/** Distinct non-empty hire ids, first-seen order. */
|
|
101
|
+
export function uniqueHireIds(raw) {
|
|
102
|
+
const out = [];
|
|
103
|
+
const seen = new Set();
|
|
104
|
+
for (const value of raw) {
|
|
105
|
+
const id = typeof value === 'string' ? value.trim() : '';
|
|
106
|
+
if (!id || seen.has(id))
|
|
107
|
+
continue;
|
|
108
|
+
seen.add(id);
|
|
109
|
+
out.push(id);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Hire ids the waking event already named. A chat-room message that
|
|
115
|
+
* carries the delivery's `agreementId` is how two customer rooms
|
|
116
|
+
* partition without a pinned ctx.
|
|
117
|
+
*/
|
|
118
|
+
export function hireIdsFromEvent(event) {
|
|
119
|
+
if (!event || typeof event !== 'object')
|
|
120
|
+
return [];
|
|
121
|
+
const rec = event;
|
|
122
|
+
const resource = rec.resource && typeof rec.resource === 'object'
|
|
123
|
+
? rec.resource
|
|
124
|
+
: {};
|
|
125
|
+
const result = rec.result && typeof rec.result === 'object'
|
|
126
|
+
? rec.result
|
|
127
|
+
: {};
|
|
128
|
+
return uniqueHireIds([
|
|
129
|
+
typeof rec.agreementId === 'string' ? rec.agreementId : null,
|
|
130
|
+
typeof resource.agreementId === 'string' ? resource.agreementId : null,
|
|
131
|
+
typeof result.agreementId === 'string' ? result.agreementId : null,
|
|
132
|
+
rec.kind === 'agreement' && typeof rec.resourceId === 'string'
|
|
133
|
+
? rec.resourceId
|
|
134
|
+
: null,
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
97
137
|
function agreementIdFromAgrnSession(sessionId) {
|
|
98
138
|
return agreementIdFromLane(sessionId)?.trim() || null;
|
|
99
139
|
}
|
|
@@ -19,12 +19,17 @@ export declare class ConnectionPool {
|
|
|
19
19
|
private readonly hosts;
|
|
20
20
|
private readonly responseListeners;
|
|
21
21
|
/**
|
|
22
|
-
* Register N agent configs. Nothing connects until `connectAll()
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* Register N agent configs. Nothing connects until `connectAll()`. A config
|
|
23
|
+
* that cannot start is reported and left down; the others still host. Only
|
|
24
|
+
* a launch where nobody came up is refused.
|
|
25
25
|
*/
|
|
26
26
|
register(configs: AgentOptions[], metaArr?: ConnectionManagerMeta[]): void;
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Connect every registered agent. One host that cannot come up is reported
|
|
29
|
+
* and does not take the rest down — a fleet where 28 of 29 are live is not
|
|
30
|
+
* a launch worth aborting. Zero started is: the process has nothing to
|
|
31
|
+
* host and must restart.
|
|
32
|
+
*/
|
|
28
33
|
connectAll(): Promise<void>;
|
|
29
34
|
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
30
35
|
startAgent(agentId: string): Promise<Agent>;
|
|
@@ -26,9 +26,9 @@ export class ConnectionPool {
|
|
|
26
26
|
// Listeners enqueued by requestFrom — drained on outbound send-message effects.
|
|
27
27
|
responseListeners = new Map();
|
|
28
28
|
/**
|
|
29
|
-
* Register N agent configs. Nothing connects until `connectAll()
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* Register N agent configs. Nothing connects until `connectAll()`. A config
|
|
30
|
+
* that cannot start is reported and left down; the others still host. Only
|
|
31
|
+
* a launch where nobody came up is refused.
|
|
32
32
|
*/
|
|
33
33
|
register(configs, metaArr = []) {
|
|
34
34
|
const metaByAgentId = new Map();
|
|
@@ -64,18 +64,28 @@ export class ConnectionPool {
|
|
|
64
64
|
runtimeLog.debug('ConnectionPool', `connected "${agentId}"`);
|
|
65
65
|
return host;
|
|
66
66
|
}, async (host) => {
|
|
67
|
-
host.disconnect();
|
|
67
|
+
await host.disconnect();
|
|
68
68
|
this.hosts.delete(agentId);
|
|
69
69
|
runtimeLog.debug('ConnectionPool', `disconnected "${agentId}"`);
|
|
70
70
|
}, metaByAgentId.get(agentId));
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
-
/**
|
|
73
|
+
/**
|
|
74
|
+
* Connect every registered agent. One host that cannot come up is reported
|
|
75
|
+
* and does not take the rest down — a fleet where 28 of 29 are live is not
|
|
76
|
+
* a launch worth aborting. Zero started is: the process has nothing to
|
|
77
|
+
* host and must restart.
|
|
78
|
+
*/
|
|
74
79
|
async connectAll() {
|
|
75
80
|
await this.manager.connectAll();
|
|
76
81
|
const connected = this.listActive().length;
|
|
82
|
+
if (this.size === 0)
|
|
83
|
+
return;
|
|
84
|
+
if (connected === 0) {
|
|
85
|
+
throw new Error(`0/${this.size} agents started; process must restart`);
|
|
86
|
+
}
|
|
77
87
|
if (connected !== this.size) {
|
|
78
|
-
|
|
88
|
+
runtimeLog.warn('ConnectionPool', `Only ${connected}/${this.size} agents started — hosting the ones that came up. One host must not take the rest down.`);
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
91
|
/** The host for one agent, connecting it if it somehow is not up yet. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkScope } from '../runtime/wakeEffects.js';
|
|
1
2
|
export interface StateTimeoutEvent {
|
|
2
3
|
type: 'state-timeout';
|
|
3
4
|
state: string;
|
|
@@ -13,7 +14,7 @@ export interface StateTimeoutEvent {
|
|
|
13
14
|
* check-back is none of that: it belongs to the agent's own plan, not to a
|
|
14
15
|
* state's declared timeout, and the Claude brain has no states at all.
|
|
15
16
|
*/
|
|
16
|
-
export interface WakeAtEvent {
|
|
17
|
+
export interface WakeAtEvent extends WorkScope {
|
|
17
18
|
type: 'wake-at';
|
|
18
19
|
/** What the agent said it was waiting for. Free text, for the brain to read. */
|
|
19
20
|
reason: string | null;
|
|
@@ -35,7 +36,7 @@ export declare const MAX_WAKE_DELAY_MS: number;
|
|
|
35
36
|
* (caller-provided enqueue → EventQueue), so it serializes behind any real
|
|
36
37
|
* event and can never race one. Epochs make stale frames identifiable: a
|
|
37
38
|
* queued timeout frame whose epoch is not the lane's currently-armed epoch was
|
|
38
|
-
*
|
|
39
|
+
* cancelled or replaced and must be dropped.
|
|
39
40
|
*
|
|
40
41
|
* In-memory only: process restart loses all timers; each lane re-arms on its
|
|
41
42
|
* next real event (sessionStore persists { ctx, state } but not timers).
|
|
@@ -66,13 +67,12 @@ export declare class StateTimers {
|
|
|
66
67
|
* the agent changing its mind, which is allowed and is why this replaces
|
|
67
68
|
* rather than adds.
|
|
68
69
|
*
|
|
69
|
-
* In memory only. A restart loses it
|
|
70
|
-
*
|
|
71
|
-
* listed Later on the ticket. Returns what actually got armed so the caller
|
|
70
|
+
* In memory only. A restart loses it; recovery requires the agent to re-arm
|
|
71
|
+
* on its next real event. Returns what actually got armed so the caller
|
|
72
72
|
* can tell the agent, since "I will wake at X" is a promise the agent will
|
|
73
73
|
* otherwise make on its own.
|
|
74
74
|
*/
|
|
75
|
-
armWakeAt(laneKey: string, at: Date | number, reason?: string | null): {
|
|
75
|
+
armWakeAt(laneKey: string, at: Date | number, reason?: string | null, scope?: WorkScope): {
|
|
76
76
|
firesAt: number;
|
|
77
77
|
delayMs: number;
|
|
78
78
|
};
|
|
@@ -82,6 +82,12 @@ export declare class StateTimers {
|
|
|
82
82
|
firesAt: number;
|
|
83
83
|
reason: string | null;
|
|
84
84
|
} | null;
|
|
85
|
+
/**
|
|
86
|
+
* Consume the timer for a frame that survived stripStale. Liveness tracks
|
|
87
|
+
* inactivity, so any event resets it. An explicit wake is an appointment:
|
|
88
|
+
* unrelated traffic must leave it armed, even after it fired and queued.
|
|
89
|
+
*/
|
|
90
|
+
consumeForEvent(event: unknown, laneKey: string): void;
|
|
85
91
|
clear(laneKey: string): void;
|
|
86
92
|
clearAll(): void;
|
|
87
93
|
/**
|
|
@@ -12,7 +12,7 @@ export const MAX_WAKE_DELAY_MS = 2 ** 31 - 1;
|
|
|
12
12
|
* (caller-provided enqueue → EventQueue), so it serializes behind any real
|
|
13
13
|
* event and can never race one. Epochs make stale frames identifiable: a
|
|
14
14
|
* queued timeout frame whose epoch is not the lane's currently-armed epoch was
|
|
15
|
-
*
|
|
15
|
+
* cancelled or replaced and must be dropped.
|
|
16
16
|
*
|
|
17
17
|
* In-memory only: process restart loses all timers; each lane re-arms on its
|
|
18
18
|
* next real event (sessionStore persists { ctx, state } but not timers).
|
|
@@ -68,17 +68,17 @@ export class StateTimers {
|
|
|
68
68
|
* the agent changing its mind, which is allowed and is why this replaces
|
|
69
69
|
* rather than adds.
|
|
70
70
|
*
|
|
71
|
-
* In memory only. A restart loses it
|
|
72
|
-
*
|
|
73
|
-
* listed Later on the ticket. Returns what actually got armed so the caller
|
|
71
|
+
* In memory only. A restart loses it; recovery requires the agent to re-arm
|
|
72
|
+
* on its next real event. Returns what actually got armed so the caller
|
|
74
73
|
* can tell the agent, since "I will wake at X" is a promise the agent will
|
|
75
74
|
* otherwise make on its own.
|
|
76
75
|
*/
|
|
77
|
-
armWakeAt(laneKey, at, reason = null) {
|
|
76
|
+
armWakeAt(laneKey, at, reason = null, scope) {
|
|
78
77
|
const firesAtRaw = at instanceof Date ? at.getTime() : Number(at);
|
|
79
78
|
if (!Number.isFinite(firesAtRaw)) {
|
|
80
79
|
throw new Error('StateTimers.armWakeAt: `at` must be a Date or epoch ms');
|
|
81
80
|
}
|
|
81
|
+
const wakeScope = { ...scope };
|
|
82
82
|
const now = Date.now();
|
|
83
83
|
// A wake already due fires on the next turn of the event loop rather than
|
|
84
84
|
// being refused: "wake me at 09:00" arriving at 09:00:01 means now, and an
|
|
@@ -92,7 +92,7 @@ export class StateTimers {
|
|
|
92
92
|
const epoch = ++this._epoch;
|
|
93
93
|
const handle = setTimeout(() => {
|
|
94
94
|
runtimeLog.debug('StateTimers', `wake-at fired laneKey=${laneKey} reason=${reason ?? 'none'} epoch=${epoch}`);
|
|
95
|
-
this._enqueue({ type: 'wake-at', reason, scheduledFor: firesAtRaw, epoch }, laneKey);
|
|
95
|
+
this._enqueue({ type: 'wake-at', reason, scheduledFor: firesAtRaw, epoch, ...wakeScope }, laneKey);
|
|
96
96
|
}, delayMs);
|
|
97
97
|
handle.unref?.();
|
|
98
98
|
this._timers.set(laneKey, {
|
|
@@ -112,6 +112,26 @@ export class StateTimers {
|
|
|
112
112
|
const t = this._timers.get(laneKey);
|
|
113
113
|
return t ? { kind: t.kind, firesAt: t.firesAt, reason: t.reason } : null;
|
|
114
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Consume the timer for a frame that survived stripStale. Liveness tracks
|
|
117
|
+
* inactivity, so any event resets it. An explicit wake is an appointment:
|
|
118
|
+
* unrelated traffic must leave it armed, even after it fired and queued.
|
|
119
|
+
*/
|
|
120
|
+
consumeForEvent(event, laneKey) {
|
|
121
|
+
const pending = this._timers.get(laneKey);
|
|
122
|
+
if (!pending)
|
|
123
|
+
return;
|
|
124
|
+
const frame = event;
|
|
125
|
+
const events = frame?.type === 'batch' && Array.isArray(frame.events)
|
|
126
|
+
? frame.events
|
|
127
|
+
: [event];
|
|
128
|
+
const consumesWake = events.some((entry) => {
|
|
129
|
+
const e = entry;
|
|
130
|
+
return e?.type === 'wake-at' && e.epoch === pending.epoch;
|
|
131
|
+
});
|
|
132
|
+
if (pending.kind === 'liveness' || consumesWake)
|
|
133
|
+
this.clear(laneKey);
|
|
134
|
+
}
|
|
115
135
|
clear(laneKey) {
|
|
116
136
|
const t = this._timers.get(laneKey);
|
|
117
137
|
if (!t)
|
|
@@ -35,7 +35,11 @@ async function buildSnapshot(deps, chatId, agentId, opts = {}) {
|
|
|
35
35
|
catch (err) {
|
|
36
36
|
// Defensive: server normally signals a dead chat as 200 + chatMissing, but
|
|
37
37
|
// a raw 404 degrades the same way so message wakes still skip it.
|
|
38
|
-
|
|
38
|
+
const status = err?.status;
|
|
39
|
+
// 403 = exists but this agent has no grant. Same as deleted:
|
|
40
|
+
// no world to act in. Leaving it as an empty snapshot used to run a
|
|
41
|
+
// full turn against refusals.
|
|
42
|
+
if (status === 404 || status === 403) {
|
|
39
43
|
return emptySnapshot(agentId, true);
|
|
40
44
|
}
|
|
41
45
|
runtimeLog.warn('ZiggsEffectHandler', `snapshot fetch failed: ${err?.message ?? err}`);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type InboxChatNews, type InboxRequestRef, type InboxTaskRef, type Creds } from '@ziggs-ai/api-client';
|
|
1
|
+
import { type InboxChatNews, type InboxDeliveryRef, 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
|
|
@@ -60,12 +60,11 @@ export interface InboxPassResult {
|
|
|
60
60
|
*/
|
|
61
61
|
export declare function chatsInWakeOrder(chats: InboxChatNews[]): InboxChatNews[];
|
|
62
62
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
* It never acks — the main pass still owns the watermark.
|
|
63
|
+
* The one hire a chat's assigned deliveries name, or null when the room
|
|
64
|
+
* has none or more than one. Memory (and acting-for) treat "several
|
|
65
|
+
* customers in one room" as the shared section.
|
|
67
66
|
*/
|
|
68
|
-
export declare function
|
|
67
|
+
export declare function soleHireAgreementIdForChat(deliveries: readonly Pick<InboxDeliveryRef, 'chatId' | 'agreementId' | 'assigneeId'>[], chatId: string, ownAgentId: string): string | null;
|
|
69
68
|
/**
|
|
70
69
|
* One long-poll pass: hold up to `waitSeconds` for new inbox content, then read
|
|
71
70
|
* the deltas, deliver them into the host, and ack. Returns what it delivered.
|
|
@@ -114,5 +113,4 @@ export declare function heldPassAfter(prev: number, pass: Pick<InboxPassResult,
|
|
|
114
113
|
*/
|
|
115
114
|
export declare function runInboxLoop(deps: InboxLoopDeps, opts?: {
|
|
116
115
|
waitSeconds?: number;
|
|
117
|
-
probePollMs?: number;
|
|
118
116
|
}): InboxLoopHandle;
|
|
@@ -5,6 +5,17 @@ const READ_PAGE = 100;
|
|
|
5
5
|
function log(label, msg) {
|
|
6
6
|
runtimeLog.info(label, msg);
|
|
7
7
|
}
|
|
8
|
+
function inboxClient(deps) {
|
|
9
|
+
return new InboxClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl, deps.creds.instanceId);
|
|
10
|
+
}
|
|
11
|
+
async function releaseInboxHostBestEffort(deps) {
|
|
12
|
+
try {
|
|
13
|
+
await inboxClient(deps).releaseHost();
|
|
14
|
+
}
|
|
15
|
+
catch (err) {
|
|
16
|
+
warn(deps.label ?? 'inboxLoop', `inbox host release failed: ${err?.message ?? err}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
8
19
|
function warn(label, msg) {
|
|
9
20
|
runtimeLog.warn(label, msg);
|
|
10
21
|
}
|
|
@@ -25,45 +36,6 @@ export function chatsInWakeOrder(chats) {
|
|
|
25
36
|
function isPublishProbeChat(chatId) {
|
|
26
37
|
return chatId.startsWith('probe_');
|
|
27
38
|
}
|
|
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
|
-
}
|
|
67
39
|
/**
|
|
68
40
|
* Standing proposals share the merged inbox visibility model with deliveries,
|
|
69
41
|
* but older envelopes predate an explicit assignee stamp. Prefer the server's
|
|
@@ -78,6 +50,22 @@ function proposalIsMine(proposal, ownAgentId) {
|
|
|
78
50
|
return (proposal.pendingApprovalPartyIds?.includes(ownAgentId) === true ||
|
|
79
51
|
proposal.proposedTo === ownAgentId);
|
|
80
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* The one hire a chat's assigned deliveries name, or null when the room
|
|
55
|
+
* has none or more than one. Memory (and acting-for) treat "several
|
|
56
|
+
* customers in one room" as the shared section.
|
|
57
|
+
*/
|
|
58
|
+
export function soleHireAgreementIdForChat(deliveries, chatId, ownAgentId) {
|
|
59
|
+
const ids = new Set();
|
|
60
|
+
for (const d of deliveries) {
|
|
61
|
+
if (d.chatId !== chatId || !d.agreementId)
|
|
62
|
+
continue;
|
|
63
|
+
if (d.assigneeId && d.assigneeId !== ownAgentId)
|
|
64
|
+
continue;
|
|
65
|
+
ids.add(d.agreementId);
|
|
66
|
+
}
|
|
67
|
+
return ids.size === 1 ? [...ids][0] : null;
|
|
68
|
+
}
|
|
81
69
|
/**
|
|
82
70
|
* Read the messages behind one chat's deliveries and hand them to the host.
|
|
83
71
|
*
|
|
@@ -85,7 +73,7 @@ function proposalIsMine(proposal, ownAgentId) {
|
|
|
85
73
|
* reads through `/context/read`, where the agent's own grants fence what it is
|
|
86
74
|
* allowed to see. A delivery reference is an address, never an authorisation.
|
|
87
75
|
*/
|
|
88
|
-
async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
|
|
76
|
+
async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt, hireAgreementId) {
|
|
89
77
|
let delivered = 0;
|
|
90
78
|
let remaining = news.count;
|
|
91
79
|
// `after` is exclusive, so step back a millisecond to include the oldest
|
|
@@ -135,6 +123,7 @@ async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
|
|
|
135
123
|
// leaving it made ClaudeBrain treat the wake as notice-only.
|
|
136
124
|
receiverId: deps.ownAgentId,
|
|
137
125
|
receiver: { id: deps.ownAgentId },
|
|
126
|
+
...(hireAgreementId ? { agreementId: hireAgreementId } : {}),
|
|
138
127
|
}));
|
|
139
128
|
if (handled)
|
|
140
129
|
delivered++;
|
|
@@ -155,7 +144,7 @@ async function deliverMessagesForChat(deps, news, earliestTs, reader, attempt) {
|
|
|
155
144
|
*/
|
|
156
145
|
export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
157
146
|
const label = deps.label ?? 'inboxLoop';
|
|
158
|
-
const inbox =
|
|
147
|
+
const inbox = inboxClient(deps);
|
|
159
148
|
const reader = new ContextReadClient(deps.creds.operatorKey, deps.creds.agentId, deps.baseUrl);
|
|
160
149
|
const envelope = await inbox.getInbox(waitSeconds > 0 ? { waitSeconds } : {});
|
|
161
150
|
let messagesDelivered = 0;
|
|
@@ -244,7 +233,7 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
244
233
|
for (const news of chatsInWakeOrder(envelope.chats ?? [])) {
|
|
245
234
|
const earliest = earliestByChat.get(news.chatId) ?? news.latestAt;
|
|
246
235
|
const failuresBefore = failures;
|
|
247
|
-
const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt);
|
|
236
|
+
const n = await deliverMessagesForChat(deps, news, earliest, reader, attempt, soleHireAgreementIdForChat(envelope.deliveries, news.chatId, deps.ownAgentId));
|
|
248
237
|
messagesDelivered += n;
|
|
249
238
|
if (n > 0)
|
|
250
239
|
log(label, `chat ${news.chatId} delivered ${n} message(s)`);
|
|
@@ -409,16 +398,6 @@ export async function pollInboxOnce(deps, waitSeconds = 0, opts = {}) {
|
|
|
409
398
|
* some defaults ship with. Lower it if your deployment sits behind a shorter one.
|
|
410
399
|
*/
|
|
411
400
|
const DEFAULT_WAIT_SECONDS = 110;
|
|
412
|
-
/**
|
|
413
|
-
* Side-poll interval for store-publish `probe_*` rooms.
|
|
414
|
-
*
|
|
415
|
-
* Default **off**. A second GET /inbox beside the main long-poll burned the
|
|
416
|
-
* shared poll ceiling: every hosted agent side-polled, probes got 429s, and
|
|
417
|
-
* Register saw ALB HTML 504s. Main pass + probe-first ordering + pinned
|
|
418
|
-
* `probe_*` deliveries are enough; set `probePollMs` only when you knowingly
|
|
419
|
-
* need the extra loop.
|
|
420
|
-
*/
|
|
421
|
-
const DEFAULT_PROBE_POLL_MS = 0;
|
|
422
401
|
const ERROR_BACKOFF_BASE_MS = 1_000;
|
|
423
402
|
const ERROR_BACKOFF_MAX_MS = 30_000;
|
|
424
403
|
/**
|
|
@@ -455,7 +434,6 @@ function sleep(ms) {
|
|
|
455
434
|
export function runInboxLoop(deps, opts = {}) {
|
|
456
435
|
const label = deps.label ?? 'inboxLoop';
|
|
457
436
|
const waitSeconds = opts.waitSeconds ?? DEFAULT_WAIT_SECONDS;
|
|
458
|
-
const probePollMs = opts.probePollMs ?? DEFAULT_PROBE_POLL_MS;
|
|
459
437
|
let running = true;
|
|
460
438
|
let consecutiveErrors = 0;
|
|
461
439
|
let heldPasses = 0;
|
|
@@ -501,32 +479,7 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
501
479
|
}
|
|
502
480
|
}
|
|
503
481
|
})();
|
|
504
|
-
const
|
|
505
|
-
if (probePollMs <= 0)
|
|
506
|
-
return;
|
|
507
|
-
while (running) {
|
|
508
|
-
const slice = 50;
|
|
509
|
-
let left = probePollMs;
|
|
510
|
-
while (left > 0 && running) {
|
|
511
|
-
const step = Math.min(slice, left);
|
|
512
|
-
await sleep(step);
|
|
513
|
-
left -= step;
|
|
514
|
-
}
|
|
515
|
-
if (!running)
|
|
516
|
-
break;
|
|
517
|
-
try {
|
|
518
|
-
const n = await answerPublishProbes(deps);
|
|
519
|
-
if (n > 0)
|
|
520
|
-
log(label, `publish-probe side poll delivered ${n} message(s)`);
|
|
521
|
-
}
|
|
522
|
-
catch (err) {
|
|
523
|
-
if (!running)
|
|
524
|
-
break;
|
|
525
|
-
warn(label, `publish-probe side poll failed: ${err?.message ?? err}`);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
})();
|
|
529
|
-
const done = Promise.all([main, probes]);
|
|
482
|
+
const done = main;
|
|
530
483
|
return {
|
|
531
484
|
get running() {
|
|
532
485
|
return running;
|
|
@@ -539,7 +492,10 @@ export function runInboxLoop(deps, opts = {}) {
|
|
|
539
492
|
},
|
|
540
493
|
async stop() {
|
|
541
494
|
running = false;
|
|
542
|
-
|
|
495
|
+
// Drop the lease immediately. Do not wait out a held poll — SIGTERM
|
|
496
|
+
// then process.exit cannot sit on the 110s wait.
|
|
497
|
+
await releaseInboxHostBestEffort(deps);
|
|
498
|
+
void done.catch(() => { });
|
|
543
499
|
},
|
|
544
500
|
};
|
|
545
501
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { AgentOptions } from '../../Agent.js';
|
|
2
2
|
import { PollRunner, type WakeTarget } from './PollRunner.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Build the runner that executes this agent's declared wakes.
|
|
5
|
+
*
|
|
6
|
+
* A malformed `polls` value is a programming error and still throws. A
|
|
7
|
+
* binding that names an unknown kind or has no chatId is not: PollRunner
|
|
8
|
+
* warns and skips it, so one agent's invalid poll cannot refuse the host.
|
|
9
|
+
*/
|
|
4
10
|
export declare function createDeclaredPollRunner(options: AgentOptions, target: WakeTarget): PollRunner | null;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { PollRegistry } from './PollKind.js';
|
|
2
2
|
import { PollRunner } from './PollRunner.js';
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Build the runner that executes this agent's declared wakes.
|
|
5
|
+
*
|
|
6
|
+
* A malformed `polls` value is a programming error and still throws. A
|
|
7
|
+
* binding that names an unknown kind or has no chatId is not: PollRunner
|
|
8
|
+
* warns and skips it, so one agent's invalid poll cannot refuse the host.
|
|
9
|
+
*/
|
|
4
10
|
export function createDeclaredPollRunner(options, target) {
|
|
5
11
|
const registry = new PollRegistry();
|
|
6
12
|
for (const kind of options.pollKinds ?? [])
|
|
@@ -9,14 +15,6 @@ export function createDeclaredPollRunner(options, target) {
|
|
|
9
15
|
return null;
|
|
10
16
|
if (!Array.isArray(options.polls))
|
|
11
17
|
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
18
|
if (!options.polls.length)
|
|
21
19
|
return null;
|
|
22
20
|
return new PollRunner({
|
|
@@ -24,6 +24,7 @@ export { CONNECTION_TOOLS, connectionProxyTool, connectionListGrantsTool, reques
|
|
|
24
24
|
export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReachTool, contextDiscoverGrantableTool, } from './tier2/contextTools.js';
|
|
25
25
|
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
26
26
|
export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
|
|
27
|
+
export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
|
|
27
28
|
export { LINK_TOOLS, linkProposeTool, linkListTool, } from './tier2/linkTools.js';
|
|
28
29
|
export { INTRODUCTION_TOOLS, introductionMintTool, introductionRedeemTool, introductionListTool, introductionRevokeTool, } from './tier2/introductionTools.js';
|
|
29
30
|
export { ARTIFACT_TOOLS, recordArtifactTool } from './tier2/artifactTools.js';
|
|
@@ -28,6 +28,7 @@ export { CONTEXT_TOOLS, contextReadTool, contextDelegateTool, contextExpandReach
|
|
|
28
28
|
// payment_list_grants). Always-available on the hosted agent — see CLAUDE_SDK_TOOLS.
|
|
29
29
|
export { GRANTS_TOOLS, listGrantsTool } from './tier2/grantsTools.js';
|
|
30
30
|
export { TASK_TOOLS, listTasksTool } from './tier2/taskTools.js';
|
|
31
|
+
export { PLAN_TOOLS, taskReplacePlanTool } from './tier2/planTools.js';
|
|
31
32
|
// links (agreements with engagementKind 'link') — cross-org reach.
|
|
32
33
|
///1022: the rail is two tools; propose/claim/revoke ride the
|
|
33
34
|
// agreement grammar.
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../../../tools/defineTool.js';
|
|
2
2
|
export declare const contextReadTool: ToolDefinition;
|
|
3
|
+
export declare const openTool: ToolDefinition;
|
|
4
|
+
export declare const accessExplainTool: ToolDefinition;
|
|
3
5
|
export declare const contextDelegateTool: ToolDefinition;
|
|
4
6
|
export declare const contextExpandReachTool: ToolDefinition;
|
|
5
7
|
export declare const contextDiscoverGrantableTool: ToolDefinition;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { CONTEXT_CAPABILITIES, contextReadCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
|
|
1
|
+
import { CONTEXT_CAPABILITIES, contextReadCapability, openCapability, accessExplainCapability, contextDelegateCapability, contextExpandReachCapability, contextDiscoverGrantableCapability, } from '@ziggs-ai/api-client';
|
|
2
2
|
import { toolFromCapability, toolsFromCapabilities } from '../../../tools/capabilityAdapter.js';
|
|
3
3
|
// context grant tools ride the same api-client
|
|
4
4
|
// clients as ziggs-mcp; the whole definition (schema + handler) is now shared
|
|
5
5
|
// via the capability layer, so the two surfaces cannot fork.
|
|
6
6
|
export const contextReadTool = toolFromCapability(contextReadCapability);
|
|
7
|
+
export const openTool = toolFromCapability(openCapability);
|
|
8
|
+
export const accessExplainTool = toolFromCapability(accessExplainCapability);
|
|
7
9
|
export const contextDelegateTool = toolFromCapability(contextDelegateCapability);
|
|
8
10
|
export const contextExpandReachTool = toolFromCapability(contextExpandReachCapability);
|
|
9
11
|
export const contextDiscoverGrantableTool = toolFromCapability(contextDiscoverGrantableCapability);
|