@tangle-network/chatgpt-agents-kit 0.1.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.
Files changed (47) hide show
  1. package/README.md +416 -0
  2. package/SETUP.md +211 -0
  3. package/dist/inspection/skills/tangle-agent-run-inspection/SKILL.md +22 -0
  4. package/dist/inspection/src/comparison.d.ts +91 -0
  5. package/dist/inspection/src/comparison.js +108 -0
  6. package/dist/inspection/src/core.d.ts +166 -0
  7. package/dist/inspection/src/core.js +236 -0
  8. package/dist/inspection/src/index.d.ts +36 -0
  9. package/dist/inspection/src/index.js +114 -0
  10. package/dist/skills/continue-agent-in-channel/SKILL.md +26 -0
  11. package/dist/skills/handoff-to-agent/SKILL.md +42 -0
  12. package/dist/skills/operate-existing-agent/SKILL.md +117 -0
  13. package/dist/skills/resolve-agent-decisions/SKILL.md +25 -0
  14. package/dist/skills/resume-agent-work/SKILL.md +32 -0
  15. package/dist/skills/review-agent-deliverables/SKILL.md +36 -0
  16. package/dist/skills/save-agent-playbook/SKILL.md +32 -0
  17. package/dist/src/app-metadata.d.ts +25 -0
  18. package/dist/src/app-metadata.js +34 -0
  19. package/dist/src/cli.d.ts +2 -0
  20. package/dist/src/cli.js +3 -0
  21. package/dist/src/connection.d.ts +10 -0
  22. package/dist/src/connection.js +25 -0
  23. package/dist/src/contracts.d.ts +161 -0
  24. package/dist/src/contracts.js +4 -0
  25. package/dist/src/events-node.d.ts +9 -0
  26. package/dist/src/events-node.js +128 -0
  27. package/dist/src/gtm.d.ts +19 -0
  28. package/dist/src/gtm.js +85 -0
  29. package/dist/src/handoff.d.ts +4 -0
  30. package/dist/src/handoff.js +112 -0
  31. package/dist/src/index.d.ts +13 -0
  32. package/dist/src/index.js +6 -0
  33. package/dist/src/inspection.d.ts +14 -0
  34. package/dist/src/inspection.js +40 -0
  35. package/dist/src/kit.d.ts +21 -0
  36. package/dist/src/kit.js +525 -0
  37. package/dist/src/package.d.ts +28 -0
  38. package/dist/src/package.js +259 -0
  39. package/dist/src/sandbox-agent.d.ts +35 -0
  40. package/dist/src/sandbox-agent.js +178 -0
  41. package/dist/src/task-events.d.ts +238 -0
  42. package/dist/src/task-events.js +268 -0
  43. package/dist/src/task-wait.d.ts +26 -0
  44. package/dist/src/task-wait.js +61 -0
  45. package/dist/src/workflows.d.ts +19 -0
  46. package/dist/src/workflows.js +38 -0
  47. package/package.json +58 -0
@@ -0,0 +1,238 @@
1
+ import type { NativeBinding, Principal, Target } from './contracts.ts';
2
+ export declare const TASK_EVENT = "agent.task_updated";
3
+ export interface EventAccess {
4
+ principal: Principal;
5
+ binding: NativeBinding;
6
+ check(): Promise<void>;
7
+ }
8
+ export interface EventOwner {
9
+ app: string;
10
+ user: string;
11
+ nativeUser: string;
12
+ client: string;
13
+ grant: string;
14
+ }
15
+ export interface Subscription {
16
+ owner: EventOwner;
17
+ target: Target;
18
+ turnId: string;
19
+ url: string;
20
+ secret: string;
21
+ previousSecret?: string;
22
+ rotateUntil?: number;
23
+ verifiedUntil: number;
24
+ epoch: string;
25
+ active: boolean;
26
+ expires: number;
27
+ nextAt: number;
28
+ lastDigest?: string;
29
+ pending?: {
30
+ digest: string;
31
+ body: string;
32
+ eventId: string;
33
+ };
34
+ attempts: number;
35
+ lastError?: string;
36
+ }
37
+ export interface EventRow {
38
+ id: string;
39
+ revision: number;
40
+ data: Subscription;
41
+ }
42
+ /** Use the host's database/vault. CAS must be atomic across processes; secrets
43
+ * must be encrypted at rest. These records are delivery state, NOT agent tasks. */
44
+ export interface EventStore {
45
+ get(id: string): Promise<EventRow | null>;
46
+ put(id: string, expectedRevision: number | null, data: Subscription): Promise<boolean>;
47
+ due(now: number, limit: number): Promise<EventRow[]>;
48
+ nextDue(): Promise<number | null>;
49
+ }
50
+ export interface WebhookResponse {
51
+ status: number;
52
+ body: string;
53
+ }
54
+ /** Must pin validated public addresses at connection time, retain TLS hostname,
55
+ * reject redirects, bound response bytes and enforce the supplied AbortSignal. */
56
+ export type WebhookPost = (url: string, body: string, headers: Record<string, string>, signal: AbortSignal) => Promise<WebhookResponse>;
57
+ export interface TaskEventsOptions {
58
+ store: EventStore;
59
+ post: WebhookPost;
60
+ requiredScopes: readonly string[];
61
+ /** Persist a native grant REFERENCE, never an access/refresh token. */
62
+ grant(access: EventAccess): Promise<string | null>;
63
+ /** Resolve the original still-active native grant with fresh identity/scopes.
64
+ * null means revoked; throw for an outage. Never reconstruct a privileged client. */
65
+ resolve(owner: EventOwner): Promise<EventAccess | null>;
66
+ /** Enqueue the existing host worker. That worker calls drain(), also after
67
+ * retained native task updates and on its existing recovery/retry cadence. */
68
+ wake(at: number): Promise<void>;
69
+ now?: () => number;
70
+ }
71
+ /** Standard Webhooks signing format; serializes no body and exposes no key. */
72
+ export declare function signedEventHeaders(id: string, subscriptionId: string, body: string, secrets: readonly string[], now: number): Promise<{
73
+ 'content-type': string;
74
+ 'webhook-id': string;
75
+ 'webhook-timestamp': string;
76
+ 'webhook-signature': string;
77
+ 'X-MCP-Subscription-Id': string;
78
+ }>;
79
+ export declare const TASK_EVENT_DEFINITION: {
80
+ name: string;
81
+ description: string;
82
+ delivery: string[];
83
+ inputSchema: {
84
+ type: string;
85
+ properties: {
86
+ workspaceId: {
87
+ type: string;
88
+ pattern: string;
89
+ };
90
+ threadId: {
91
+ type: string;
92
+ pattern: string;
93
+ };
94
+ turnId: {
95
+ type: string;
96
+ pattern: string;
97
+ };
98
+ };
99
+ required: string[];
100
+ additionalProperties: boolean;
101
+ };
102
+ payloadSchema: {
103
+ type: string;
104
+ properties: {
105
+ workspaceId: {
106
+ type: string;
107
+ pattern: string;
108
+ };
109
+ threadId: {
110
+ type: string;
111
+ pattern: string;
112
+ };
113
+ turnId: {
114
+ type: string;
115
+ pattern: string;
116
+ };
117
+ state: {
118
+ type: string;
119
+ enum: string[];
120
+ };
121
+ executionId: {
122
+ type: string[];
123
+ };
124
+ completionVerified: {
125
+ const: boolean;
126
+ };
127
+ };
128
+ required: string[];
129
+ additionalProperties: boolean;
130
+ };
131
+ };
132
+ export declare function createTaskEvents(options: TaskEventsOptions): {
133
+ available: (access: EventAccess) => Promise<boolean>;
134
+ rpc: (method: string, raw: unknown, access: EventAccess) => Promise<{
135
+ result: {
136
+ events: {
137
+ name: string;
138
+ description: string;
139
+ delivery: string[];
140
+ inputSchema: {
141
+ type: string;
142
+ properties: {
143
+ workspaceId: {
144
+ type: string;
145
+ pattern: string;
146
+ };
147
+ threadId: {
148
+ type: string;
149
+ pattern: string;
150
+ };
151
+ turnId: {
152
+ type: string;
153
+ pattern: string;
154
+ };
155
+ };
156
+ required: string[];
157
+ additionalProperties: boolean;
158
+ };
159
+ payloadSchema: {
160
+ type: string;
161
+ properties: {
162
+ workspaceId: {
163
+ type: string;
164
+ pattern: string;
165
+ };
166
+ threadId: {
167
+ type: string;
168
+ pattern: string;
169
+ };
170
+ turnId: {
171
+ type: string;
172
+ pattern: string;
173
+ };
174
+ state: {
175
+ type: string;
176
+ enum: string[];
177
+ };
178
+ executionId: {
179
+ type: string[];
180
+ };
181
+ completionVerified: {
182
+ const: boolean;
183
+ };
184
+ };
185
+ required: string[];
186
+ additionalProperties: boolean;
187
+ };
188
+ }[];
189
+ id?: undefined;
190
+ refreshBefore?: undefined;
191
+ cursor?: undefined;
192
+ truncated?: undefined;
193
+ };
194
+ error?: undefined;
195
+ } | {
196
+ error: {
197
+ code: number;
198
+ message: string;
199
+ data?: undefined;
200
+ };
201
+ result?: undefined;
202
+ } | {
203
+ result: {
204
+ events?: undefined;
205
+ id?: undefined;
206
+ refreshBefore?: undefined;
207
+ cursor?: undefined;
208
+ truncated?: undefined;
209
+ };
210
+ error?: undefined;
211
+ } | {
212
+ result: {
213
+ id: string;
214
+ refreshBefore: string;
215
+ cursor: null;
216
+ truncated: boolean;
217
+ events?: undefined;
218
+ };
219
+ error?: undefined;
220
+ } | {
221
+ error: {
222
+ code: number;
223
+ message: string;
224
+ data: {
225
+ reason: string;
226
+ };
227
+ };
228
+ result?: undefined;
229
+ }>;
230
+ drain: (limit?: number) => Promise<{
231
+ nextWakeAt: number | null;
232
+ examined: number;
233
+ accepted: number;
234
+ stopped: number;
235
+ deferred: number;
236
+ }>;
237
+ };
238
+ export type TaskEvents = ReturnType<typeof createTaskEvents>;
@@ -0,0 +1,268 @@
1
+ import { AgentFailure } from "./contracts.js";
2
+ import { validateNativeTask } from "./task-wait.js";
3
+ export const TASK_EVENT = 'agent.task_updated';
4
+ class EventFailure extends Error {
5
+ code;
6
+ reason;
7
+ constructor(code, reason) { super(reason); this.code = code; this.reason = reason; }
8
+ }
9
+ const encoder = new TextEncoder();
10
+ const hash = async (text) => Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', encoder.encode(text))), x => x.toString(16).padStart(2, '0')).join('');
11
+ const object = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
12
+ function keys(v, allowed) {
13
+ if (Object.keys(v).some(k => !allowed.includes(k)))
14
+ throw new EventFailure(-32602, 'invalid_arguments');
15
+ }
16
+ function signingKey(secret) {
17
+ if (!/^whsec_[A-Za-z0-9+/]+={0,2}$/.test(secret))
18
+ throw new EventFailure(-32602, 'invalid_signing_secret');
19
+ let bytes;
20
+ try {
21
+ bytes = Uint8Array.from(atob(secret.slice(6)), c => c.charCodeAt(0));
22
+ }
23
+ catch {
24
+ throw new EventFailure(-32602, 'invalid_signing_secret');
25
+ }
26
+ if (bytes.length < 24 || bytes.length > 64 || btoa(String.fromCharCode(...bytes)) !== secret.slice(6))
27
+ throw new EventFailure(-32602, 'invalid_signing_secret');
28
+ return bytes;
29
+ }
30
+ /** Standard Webhooks signing format; serializes no body and exposes no key. */
31
+ export async function signedEventHeaders(id, subscriptionId, body, secrets, now) {
32
+ if (encoder.encode(body).length > 262144)
33
+ throw new EventFailure(-32602, 'payload_too_large');
34
+ const timestamp = String(Math.floor(now / 1000));
35
+ const signatures = await Promise.all(secrets.map(async (secret) => {
36
+ const key = await crypto.subtle.importKey('raw', signingKey(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
37
+ const bytes = new Uint8Array(await crypto.subtle.sign('HMAC', key, encoder.encode(`${id}.${timestamp}.${body}`)));
38
+ return `v1,${btoa(String.fromCharCode(...bytes))}`;
39
+ }));
40
+ return { 'content-type': 'application/json', 'webhook-id': id, 'webhook-timestamp': timestamp,
41
+ 'webhook-signature': signatures.join(' '), 'X-MCP-Subscription-Id': subscriptionId };
42
+ }
43
+ function equalChallenge(a, b) {
44
+ if (typeof a !== 'string')
45
+ return false;
46
+ const left = encoder.encode(a), right = encoder.encode(b);
47
+ let diff = left.length ^ right.length;
48
+ for (let i = 0; i < right.length; i++)
49
+ diff |= (left[i] ?? 0) ^ right[i];
50
+ return diff === 0;
51
+ }
52
+ const idSchema = { type: 'string', pattern: '^[A-Za-z0-9_-]{1,256}$' };
53
+ export const TASK_EVENT_DEFINITION = {
54
+ name: TASK_EVENT,
55
+ description: 'A retained Tangle task has a newly observed state. Filter one exact workspace, thread and turn. Retrieve get_task for current verified outputs; events never authorize actions.',
56
+ delivery: ['webhook'],
57
+ inputSchema: { type: 'object', properties: { workspaceId: idSchema, threadId: idSchema, turnId: idSchema }, required: ['workspaceId', 'threadId', 'turnId'], additionalProperties: false },
58
+ payloadSchema: { type: 'object', properties: { workspaceId: idSchema, threadId: idSchema, turnId: idSchema,
59
+ state: { type: 'string', enum: ['queued', 'working', 'input-required', 'succeeded', 'failed', 'cancelled', 'unknown'] },
60
+ executionId: { type: ['string', 'null'] }, completionVerified: { const: false } },
61
+ required: ['workspaceId', 'threadId', 'turnId', 'state', 'executionId', 'completionVerified'], additionalProperties: false },
62
+ };
63
+ export function createTaskEvents(options) {
64
+ if (!options.requiredScopes.length || options.requiredScopes.some(s => !/^[\x21\x23-\x5B\x5D-\x7E]{1,256}$/.test(s)))
65
+ throw new Error('events_scopes_required');
66
+ const now = options.now ?? Date.now;
67
+ async function available(access) {
68
+ const b = access.binding;
69
+ const taskScopes = b.requiredScopes.task, outputScopes = b.requiredScopes.output;
70
+ return b.platformUserId === access.principal.userId && !!b.services.getTask && (!!b.services.readFile || !!b.services.readResponse)
71
+ && taskScopes !== undefined && outputScopes !== undefined
72
+ && [...options.requiredScopes, ...taskScopes, ...outputScopes].every(s => access.principal.scopes.includes(s));
73
+ }
74
+ async function ownerOf(access) {
75
+ await access.check();
76
+ const grant = await options.grant(access);
77
+ const client = access.principal.clientId;
78
+ if (!grant || !client || grant.length > 256 || client.length > 1024)
79
+ throw new EventFailure(-32001, 'events_grant_unavailable');
80
+ return { app: access.binding.appName, user: access.principal.userId, nativeUser: access.binding.applicationUserId, client, grant };
81
+ }
82
+ async function checked(owner, target) {
83
+ const access = await options.resolve(owner);
84
+ if (!access)
85
+ return null;
86
+ if (!await available(access) || JSON.stringify(await ownerOf(access)) !== JSON.stringify(owner))
87
+ return null;
88
+ await access.binding.authorize(target, 'task');
89
+ await access.check();
90
+ return access;
91
+ }
92
+ const identity = (owner, url, target, turnId) => hash(JSON.stringify([owner.app, owner.user, owner.nativeUser, owner.client, owner.grant, url, TASK_EVENT, target.workspaceId, target.threadId, turnId]));
93
+ async function rpc(method, raw, access) {
94
+ try {
95
+ if (!object(raw))
96
+ throw new EventFailure(-32602, 'invalid_arguments');
97
+ if (method === 'events/list') {
98
+ keys(raw, ['cursor', '_meta']);
99
+ if (raw.cursor != null)
100
+ throw new EventFailure(-32602, 'cursor_unsupported');
101
+ return { result: { events: await available(access) ? [TASK_EVENT_DEFINITION] : [] } };
102
+ }
103
+ if (!['events/subscribe', 'events/unsubscribe'].includes(method))
104
+ return { error: { code: -32601, message: 'Method not found' } };
105
+ keys(raw, ['name', 'arguments', 'delivery', 'cursor', 'ttlMs', '_meta']);
106
+ if (raw.name !== TASK_EVENT || !object(raw.arguments) || !object(raw.delivery))
107
+ throw new EventFailure(-32602, 'invalid_arguments');
108
+ keys(raw.arguments, ['workspaceId', 'threadId', 'turnId']);
109
+ keys(raw.delivery, method === 'events/subscribe' ? ['mode', 'url', 'secret'] : ['mode', 'url']);
110
+ const { workspaceId, threadId, turnId } = raw.arguments;
111
+ if (![workspaceId, threadId, turnId].every(v => typeof v === 'string' && /^[A-Za-z0-9_-]{1,256}$/.test(v)))
112
+ throw new EventFailure(-32602, 'invalid_target');
113
+ const target = { workspaceId: workspaceId, threadId: threadId };
114
+ if (raw.delivery.mode !== 'webhook' || typeof raw.delivery.url !== 'string' || raw.delivery.url.length > 2048)
115
+ throw new EventFailure(-32602, 'invalid_delivery');
116
+ const url = new URL(raw.delivery.url);
117
+ if (url.protocol !== 'https:' || url.username || url.password || url.hash)
118
+ throw new EventFailure(-32602, 'invalid_callback_url');
119
+ const owner = await ownerOf(access);
120
+ const id = `sub_${await identity(owner, url.href, target, turnId)}`;
121
+ const previous = await options.store.get(id);
122
+ if (method === 'events/unsubscribe') {
123
+ // An owner can stop delivery even after workspace access is removed.
124
+ const stopped = previous ? { ...previous.data, active: false, secret: '', previousSecret: undefined, pending: undefined, nextAt: now() }
125
+ : { owner, target, turnId: turnId, url: url.href, secret: '', epoch: crypto.randomUUID(), active: false, expires: now(), verifiedUntil: 0, nextAt: now(), attempts: 0 };
126
+ if (!await options.store.put(id, previous?.revision ?? null, stopped))
127
+ throw new EventFailure(-32000, 'subscription_conflict_retry');
128
+ return { result: {} };
129
+ }
130
+ if (!await available(access))
131
+ throw new EventFailure(-32001, 'events_not_authorized');
132
+ await access.binding.authorize(target, 'task');
133
+ await access.check();
134
+ if (raw.cursor != null)
135
+ throw new EventFailure(-32602, 'replay_unsupported');
136
+ if (typeof raw.delivery.secret !== 'string')
137
+ throw new EventFailure(-32602, 'invalid_signing_secret');
138
+ signingKey(raw.delivery.secret);
139
+ if (raw.ttlMs != null && (!Number.isSafeInteger(raw.ttlMs) || raw.ttlMs <= 0))
140
+ throw new EventFailure(-32602, 'invalid_ttl');
141
+ const time = now(), ttl = Math.min(typeof raw.ttlMs === 'number' ? raw.ttlMs : 86400000, 86400000);
142
+ const retained = previous?.data.active && previous.data.expires > time ? previous.data : undefined;
143
+ // Cache only this principal + URL + verified key, never share verification across tenants.
144
+ if (!retained || retained.secret !== raw.delivery.secret || retained.verifiedUntil <= time) {
145
+ const challenge = crypto.randomUUID(), body = JSON.stringify({ type: 'verification', challenge });
146
+ try {
147
+ const response = await options.post(url.href, body, await signedEventHeaders(`verify_${crypto.randomUUID()}`, id, body, [raw.delivery.secret], time), AbortSignal.timeout(10000));
148
+ if (response.status < 200 || response.status >= 300 || !equalChallenge(JSON.parse(response.body).challenge, challenge))
149
+ throw new Error('challenge_failed');
150
+ }
151
+ catch {
152
+ throw new EventFailure(-32015, 'challenge_failed');
153
+ }
154
+ }
155
+ await access.binding.authorize(target, 'task');
156
+ await access.check();
157
+ const data = { owner, target, turnId: turnId, url: url.href, secret: raw.delivery.secret,
158
+ epoch: retained?.epoch ?? crypto.randomUUID(), active: true, expires: now() + ttl, verifiedUntil: now() + 300000,
159
+ nextAt: now(), attempts: 0, lastDigest: retained?.lastDigest, pending: retained?.pending,
160
+ ...(retained && retained.secret !== raw.delivery.secret ? { previousSecret: retained.secret, rotateUntil: now() + 300000 }
161
+ : retained?.previousSecret && (retained.rotateUntil ?? 0) > now() ? { previousSecret: retained.previousSecret, rotateUntil: retained.rotateUntil } : {}), };
162
+ if (!await options.store.put(id, previous?.revision ?? null, data))
163
+ throw new EventFailure(-32000, 'subscription_conflict_retry');
164
+ await options.wake(now());
165
+ return { result: { id, refreshBefore: new Date(data.expires).toISOString(), cursor: null, truncated: false } };
166
+ }
167
+ catch (error) {
168
+ return { error: { code: error instanceof EventFailure ? error.code : -32000,
169
+ message: error instanceof EventFailure && error.code === -32015 ? 'CallbackEndpointError' : 'Event operation failed',
170
+ data: { reason: error instanceof EventFailure ? error.reason : 'event_operation_failed' } } };
171
+ }
172
+ }
173
+ /** One bounded pass, called by the host's EXISTING worker; no timer, second
174
+ * scheduler or process-local task store. Reads native state to close the
175
+ * subscribe/completion race and reconcile restarts. No event-history replay. */
176
+ async function drain(limit = 32) {
177
+ if (!Number.isInteger(limit) || limit < 1 || limit > 128)
178
+ throw new Error('invalid_event_batch');
179
+ const summary = { examined: 0, accepted: 0, stopped: 0, deferred: 0 };
180
+ for (const row of await options.store.due(now(), limit)) {
181
+ const leased = { ...row.data, nextAt: now() + 30000 };
182
+ if (!await options.store.put(row.id, row.revision, leased))
183
+ continue;
184
+ const revision = row.revision + 1;
185
+ summary.examined++;
186
+ let next = { ...leased };
187
+ try {
188
+ if (!leased.active || leased.expires <= now()) {
189
+ next = { ...leased, active: false, secret: '', previousSecret: undefined, pending: undefined };
190
+ summary.stopped++;
191
+ }
192
+ else {
193
+ const access = await checked(leased.owner, leased.target);
194
+ if (!access) {
195
+ next = { ...leased, active: false, secret: '', previousSecret: undefined, pending: undefined };
196
+ summary.stopped++;
197
+ }
198
+ else {
199
+ if (!next.pending) {
200
+ const task = await access.binding.services.getTask(leased.target, leased.turnId);
201
+ validateNativeTask(task, leased.target, leased.turnId);
202
+ const data = { ...leased.target, turnId: leased.turnId, state: task.state, executionId: task.executionId ?? null, completionVerified: false };
203
+ if (data.executionId !== null && (typeof data.executionId !== 'string' || data.executionId.length > 1024))
204
+ throw new Error('native_task_invalid');
205
+ const digest = await hash(JSON.stringify([data, task.completedAt ?? null, task.outputs ?? []]));
206
+ if (digest !== leased.lastDigest) {
207
+ const eventId = `evt_${await hash(`${row.id}:${leased.epoch}:${digest}`)}`;
208
+ next.pending = { digest, eventId, body: JSON.stringify({ eventId, name: TASK_EVENT, timestamp: new Date(now()).toISOString(), data, cursor: null }) };
209
+ }
210
+ }
211
+ if (next.pending) {
212
+ // Persist the exact event bytes BEFORE network I/O. Crash/retry uses the same eventId.
213
+ if (!await options.store.put(row.id, revision, next))
214
+ continue;
215
+ const accessNow = await checked(leased.owner, leased.target);
216
+ const current = await options.store.get(row.id);
217
+ if (!current || current.revision !== revision + 1 || !current.data.active || current.data.expires <= now())
218
+ continue;
219
+ if (!accessNow) {
220
+ await options.store.put(row.id, current.revision, { ...next, active: false, secret: '', previousSecret: undefined, pending: undefined });
221
+ summary.stopped++;
222
+ continue;
223
+ }
224
+ const secrets = [next.secret, ...(next.previousSecret && (next.rotateUntil ?? 0) > now() ? [next.previousSecret] : [])];
225
+ let status = 0;
226
+ try {
227
+ status = (await options.post(next.url, next.pending.body, await signedEventHeaders(next.pending.eventId, row.id, next.pending.body, secrets, now()), AbortSignal.timeout(10000))).status;
228
+ }
229
+ catch { /* bounded retry, no upstream diagnostics */ }
230
+ if (status >= 200 && status < 300) {
231
+ next = { ...next, lastDigest: next.pending.digest, pending: undefined, attempts: 0, nextAt: now() + 10000, lastError: undefined };
232
+ summary.accepted++;
233
+ }
234
+ else if (status === 410 || status === 413 || (status >= 400 && status < 500 && status !== 408 && status !== 429) || next.attempts >= 7) {
235
+ next = { ...next, active: false, secret: '', previousSecret: undefined, pending: undefined, lastError: `callback_${status || 'unavailable'}` };
236
+ summary.stopped++;
237
+ }
238
+ else {
239
+ next = { ...next, attempts: next.attempts + 1, nextAt: now() + Math.min(300000, 1000 * 2 ** next.attempts), lastError: `callback_${status || 'unavailable'}` };
240
+ summary.deferred++;
241
+ }
242
+ await options.store.put(row.id, revision + 1, next);
243
+ continue;
244
+ }
245
+ next.nextAt = now() + 10000;
246
+ }
247
+ }
248
+ }
249
+ catch (error) {
250
+ if (error instanceof AgentFailure && [401, 403].includes(error.status)) {
251
+ next = { ...next, active: false, secret: '', previousSecret: undefined, pending: undefined };
252
+ summary.stopped++;
253
+ }
254
+ else {
255
+ next.nextAt = now() + 30000;
256
+ next.lastError = 'native_observation_unavailable';
257
+ summary.deferred++;
258
+ }
259
+ }
260
+ await options.store.put(row.id, revision, next);
261
+ }
262
+ const nextWakeAt = await options.store.nextDue();
263
+ if (nextWakeAt !== null)
264
+ await options.wake(Math.max(now() + 10, nextWakeAt));
265
+ return { ...summary, nextWakeAt };
266
+ }
267
+ return { available, rpc, drain };
268
+ }
@@ -0,0 +1,26 @@
1
+ import type { NativeTask, Target } from './contracts.ts';
2
+ /** A bounded observation of an existing native task, never an execution driver. */
3
+ export interface TaskWaitPolicy {
4
+ defaultMs?: number;
5
+ maxMs?: number;
6
+ intervalMs?: number;
7
+ }
8
+ export declare const MAX_TASK_WAIT_MS = 25000;
9
+ export declare function taskWaitPolicy(policy?: TaskWaitPolicy): {
10
+ maxMs: number;
11
+ defaultMs: number;
12
+ intervalMs: number;
13
+ };
14
+ export declare function validateNativeTask(task: NativeTask, target: Target, turnId: string): void;
15
+ /** Stops starting reads at the deadline. Hosts must bound their native read I/O;
16
+ * abandoning a Promise would leak in-flight work and is deliberately not used. */
17
+ export declare function observeTask(options: {
18
+ target: Target;
19
+ turnId: string;
20
+ initial: NativeTask;
21
+ waitMs: number;
22
+ intervalMs: number;
23
+ signal: AbortSignal;
24
+ check(): Promise<void>;
25
+ read(): Promise<NativeTask>;
26
+ }): Promise<NativeTask>;
@@ -0,0 +1,61 @@
1
+ import { projectEnvironment } from "./connection.js";
2
+ import { AgentFailure } from "./contracts.js";
3
+ export const MAX_TASK_WAIT_MS = 25_000;
4
+ export function taskWaitPolicy(policy = {}) {
5
+ const maxMs = policy.maxMs ?? MAX_TASK_WAIT_MS;
6
+ const defaultMs = policy.defaultMs ?? Math.min(15_000, maxMs);
7
+ const intervalMs = policy.intervalMs ?? 500;
8
+ if (![maxMs, defaultMs, intervalMs].every(Number.isSafeInteger) || maxMs < 0 || maxMs > MAX_TASK_WAIT_MS
9
+ || defaultMs < 0 || defaultMs > maxMs || intervalMs < 10 || intervalMs > 5_000)
10
+ throw new Error('invalid_task_wait_policy');
11
+ return { maxMs, defaultMs, intervalMs };
12
+ }
13
+ export function validateNativeTask(task, target, turnId) {
14
+ if (!task || task.workspaceId !== target.workspaceId || task.threadId !== target.threadId || task.turnId !== turnId
15
+ || typeof task.source !== 'string' || !task.source || task.source.length > 2048
16
+ || (task.executionId !== undefined && (typeof task.executionId !== 'string' || !task.executionId || task.executionId.length > 1024)) || !['queued', 'working', 'input-required', 'succeeded', 'failed', 'cancelled', 'unknown'].includes(task.state))
17
+ throw new AgentFailure('native_task_invalid', 502);
18
+ }
19
+ function pause(ms, signal) {
20
+ return new Promise((resolve, reject) => {
21
+ const abort = () => { clearTimeout(timer); reject(new AgentFailure('observation_cancelled', 499)); };
22
+ const timer = setTimeout(() => { signal.removeEventListener('abort', abort); resolve(); }, ms);
23
+ signal.addEventListener('abort', abort, { once: true });
24
+ if (signal.aborted) {
25
+ signal.removeEventListener('abort', abort);
26
+ abort();
27
+ }
28
+ });
29
+ }
30
+ /** Stops starting reads at the deadline. Hosts must bound their native read I/O;
31
+ * abandoning a Promise would leak in-flight work and is deliberately not used. */
32
+ export async function observeTask(options) {
33
+ const deadline = performance.now() + options.waitMs;
34
+ let task = options.initial;
35
+ let executionId = task.executionId;
36
+ let environment = task.environment ? JSON.stringify(projectEnvironment(task.environment)) : undefined;
37
+ validateNativeTask(task, options.target, options.turnId);
38
+ while (['queued', 'working'].includes(task.state) && performance.now() < deadline) {
39
+ if (options.signal.aborted)
40
+ throw new AgentFailure('observation_cancelled', 499);
41
+ await pause(Math.min(options.intervalMs, Math.max(1, deadline - performance.now())), options.signal);
42
+ if (performance.now() >= deadline)
43
+ break;
44
+ await options.check();
45
+ if (options.signal.aborted)
46
+ throw new AgentFailure('observation_cancelled', 499);
47
+ task = await options.read();
48
+ validateNativeTask(task, options.target, options.turnId);
49
+ if (executionId && task.executionId !== executionId)
50
+ throw new AgentFailure('native_execution_changed', 502);
51
+ executionId ??= task.executionId;
52
+ const observedEnvironment = task.environment ? JSON.stringify(projectEnvironment(task.environment)) : undefined;
53
+ if (environment && observedEnvironment !== environment)
54
+ throw new AgentFailure('native_environment_changed', 502);
55
+ environment ??= observedEnvironment;
56
+ await options.check();
57
+ }
58
+ if (options.signal.aborted)
59
+ throw new AgentFailure('observation_cancelled', 499);
60
+ return task;
61
+ }
@@ -0,0 +1,19 @@
1
+ import type { Action } from './contracts.ts';
2
+ /** One vocabulary for native tool discovery, packaging and workflow selection. */
3
+ export declare const ACTION_TOOLS: Readonly<Record<Action, string>>;
4
+ export interface AgentWorkflow {
5
+ id: string;
6
+ title: string;
7
+ prompt: string;
8
+ requires: readonly Action[];
9
+ }
10
+ export declare const HANDOFF_ACTIONS: readonly Action[];
11
+ /** These compose existing actions. They neither register tools nor grant access. */
12
+ export declare const AGENT_WORKFLOWS: readonly AgentWorkflow[];
13
+ export declare function selectAgentWorkflows(actions: readonly Action[]): AgentWorkflow[];
14
+ export declare function describeAgentWorkflows(actions: readonly Action[]): {
15
+ id: string;
16
+ title: string;
17
+ prompt: string;
18
+ requiredTools: string[];
19
+ }[];
@@ -0,0 +1,38 @@
1
+ /** One vocabulary for native tool discovery, packaging and workflow selection. */
2
+ export const ACTION_TOOLS = {
3
+ list: 'list_my_agents', connect: 'connect_my_agent', create: 'create_an_agent',
4
+ handoff: 'handoff_file', delegate: 'prompt_agent', task: 'get_task', output: 'read_output',
5
+ decisions: 'list_pending_decisions', decide: 'respond_to_decision', channel: 'continue_in_channel', cancel: 'cancel_agent_run',
6
+ };
7
+ export const HANDOFF_ACTIONS = ['connect', 'handoff', 'output', 'delegate', 'task'];
8
+ /** These compose existing actions. They neither register tools nor grant access. */
9
+ export const AGENT_WORKFLOWS = [
10
+ { id: 'handoff-to-agent', title: 'Hand this work to my agent',
11
+ prompt: 'Prepare a reviewed handoff of this work to my existing agent.',
12
+ requires: HANDOFF_ACTIONS },
13
+ { id: 'review-agent-deliverables', title: 'Review and improve my agent’s work',
14
+ prompt: 'Retrieve my agent’s actual deliverables and help me review and revise them.',
15
+ requires: ['connect', 'task', 'delegate'] },
16
+ { id: 'save-agent-playbook', title: 'Save this workflow for my agent',
17
+ prompt: 'Turn this successful workflow into a reusable playbook for my agent to review.',
18
+ requires: ['connect', 'handoff', 'output'] },
19
+ { id: 'resume-agent-work', title: 'Pick up where my agent left off',
20
+ prompt: 'Reconnect to the same agent and task, and show verified results and remaining work.',
21
+ requires: ['connect', 'task'] },
22
+ { id: 'resolve-agent-decisions', title: 'Resolve my agent’s pending decisions',
23
+ prompt: 'Show the pending decisions for my selected agent so I can review them.',
24
+ requires: ['connect', 'decisions', 'decide'] },
25
+ { id: 'continue-agent-in-channel', title: 'Continue with the same agent by message',
26
+ prompt: 'Continue this task through an already connected messaging channel.',
27
+ requires: ['connect', 'channel'] },
28
+ ];
29
+ export function selectAgentWorkflows(actions) {
30
+ const available = new Set(actions);
31
+ return AGENT_WORKFLOWS.filter(workflow => workflow.requires.every(action => available.has(action)))
32
+ .map(workflow => ({ ...workflow, requires: [...workflow.requires] }));
33
+ }
34
+ export function describeAgentWorkflows(actions) {
35
+ return selectAgentWorkflows(actions).map(({ id, title, prompt, requires }) => ({
36
+ id, title, prompt, requiredTools: requires.map(action => ACTION_TOOLS[action]),
37
+ }));
38
+ }