@agent-compose/sdk 0.5.7 → 0.5.9
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/__tests__/run-agent-liveness.test.d.ts +17 -0
- package/dist/agent/agent-context.d.ts +67 -0
- package/dist/agent/agent-loop.d.ts +23 -12
- package/dist/agent/local-pause-request.d.ts +49 -0
- package/dist/agent/local-pause-request.test.d.ts +1 -0
- package/dist/agent/steer-control.d.ts +22 -6
- package/dist/client.d.ts +76 -2
- package/dist/index.d.ts +10 -5
- package/dist/index.js +2409 -1457
- package/dist/pause/checkpoint.d.ts +27 -10
- package/dist/pause/manager.d.ts +1 -0
- package/dist/pause/pause-core.d.ts +23 -0
- package/dist/pause/state-dir.d.ts +1 -1
- package/dist/pause/wrappers.d.ts +7 -11
- package/dist/processors/builtins.d.ts +20 -1
- package/dist/processors/index.d.ts +1 -1
- package/dist/processors/processor.d.ts +13 -0
- package/dist/runtimes/_acp-client.d.ts +140 -0
- package/dist/runtimes/_cli-agent.d.ts +155 -3
- package/dist/runtimes/amp.d.ts +2 -2
- package/dist/runtimes/cli-agent-acp-live.test.d.ts +30 -0
- package/dist/runtimes/cli-agent.test.d.ts +22 -6
- package/dist/runtimes/codex.d.ts +7 -2
- package/dist/runtimes/openai-desktop.js +2394 -1457
- package/dist/runtimes/vercel.js +389 -2
- package/dist/sandbox.d.ts +132 -14
- package/dist/step-invocation/types.d.ts +1 -1
- package/dist/types/__tests__/environment-build-flag.test.d.ts +1 -0
- package/dist/types/__tests__/workflow-metadata-provider.test.d.ts +1 -0
- package/dist/types/execution-context.d.ts +1 -11
- package/dist/types/protocol.d.ts +32 -1
- package/dist/types/runtime.d.ts +7 -0
- package/dist/types/sandbox-environment.d.ts +6 -1
- package/dist/types/sandbox.d.ts +41 -6
- package/dist/types/workflow-metadata.d.ts +47 -6
- package/dist/types/workflow.d.ts +27 -4
- package/dist/utils/bundler.d.ts +7 -1
- package/dist/workflow-steps/observability.d.ts +28 -2
- package/dist/workflow-steps/types.d.ts +11 -7
- package/dist/workflow-steps/workflow.d.ts +5 -1
- package/package.json +3 -2
- package/src/agent/agent-context.ts +220 -0
- package/src/agent/agent-loop.ts +90 -22
- package/src/agent/local-pause-request.ts +90 -0
- package/src/agent/run-agent.ts +43 -3
- package/src/agent/steer-control.ts +21 -7
- package/src/client.ts +123 -2
- package/src/index.ts +16 -4
- package/src/pause/checkpoint.ts +33 -14
- package/src/pause/manager.ts +2 -2
- package/src/pause/pause-core.ts +35 -0
- package/src/pause/state-dir.ts +2 -2
- package/src/pause/wrappers.ts +7 -21
- package/src/processors/builtins.ts +44 -1
- package/src/processors/index.ts +1 -0
- package/src/processors/processor.ts +13 -0
- package/src/runtimes/_acp-client.ts +516 -0
- package/src/runtimes/_cli-agent.ts +418 -3
- package/src/runtimes/claude.ts +27 -3
- package/src/runtimes/codex.ts +21 -1
- package/src/runtimes/vercel.ts +4 -1
- package/src/sandbox.ts +429 -67
- package/src/step-invocation/types.ts +1 -1
- package/src/types/execution-context.ts +1 -11
- package/src/types/protocol.ts +27 -1
- package/src/types/runtime.ts +7 -0
- package/src/types/sandbox-environment.ts +12 -1
- package/src/types/sandbox.ts +40 -6
- package/src/types/workflow-metadata.ts +51 -6
- package/src/types/workflow.ts +27 -6
- package/src/utils/bundler.ts +9 -1
- package/src/workflow-steps/observability.ts +51 -5
- package/src/workflow-steps/runner.ts +9 -5
- package/src/workflow-steps/types.ts +11 -7
- package/src/workflow-steps/workflow.ts +5 -1
- package/src/workflows/invoke-child.ts +7 -1
package/src/client.ts
CHANGED
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
import { ofetch } from "ofetch";
|
|
15
15
|
import { AgentComposeError } from "./errors.js";
|
|
16
16
|
import { parseSseStream } from "./sse.js";
|
|
17
|
-
import type { SandboxNetworkPolicy } from "./sandbox.js";
|
|
17
|
+
import type { SandboxNetworkPolicy, SandboxSize } from "./sandbox.js";
|
|
18
18
|
import type { RunEvent } from "./types/events.js";
|
|
19
19
|
import type { WorkflowPlan } from "./types/workflow-plan.js";
|
|
20
|
-
import type { SnapshotConfig, IOSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy } from "./types/workflow-metadata.js";
|
|
20
|
+
import type { SnapshotConfig, IOSchema, ConnectorRequirements, ConnectorOperationTag, InvokePolicy, SandboxResources } from "./types/workflow-metadata.js";
|
|
21
21
|
import type { WorkflowManifest } from "./utils/bundler.js";
|
|
22
22
|
|
|
23
23
|
/** UUID-v4-ish — matches the server-side predicate. Used to auto-detect
|
|
@@ -103,6 +103,9 @@ export interface RegisterWorkflowInput {
|
|
|
103
103
|
/** All snapshot config — `bootFrom` (where to restore at run start),
|
|
104
104
|
* `save`, `retain`. See `WorkflowMetadata.snapshots`. */
|
|
105
105
|
snapshots?: SnapshotConfig;
|
|
106
|
+
/** Sandbox machine resources — size + provider (template defaults).
|
|
107
|
+
* See `WorkflowMetadata.resources`. */
|
|
108
|
+
resources?: SandboxResources;
|
|
106
109
|
/** Provider-neutral execution plan detected by the CLI bundler. */
|
|
107
110
|
workflowPlan?: WorkflowPlan;
|
|
108
111
|
/** Connector requirements declared via `defineWorkflow({ connectors })`
|
|
@@ -119,6 +122,10 @@ export interface RegisterWorkflowInput {
|
|
|
119
122
|
inputSchema?: IOSchema;
|
|
120
123
|
/** Output schema extracted from the workflow's `output` zod schema. */
|
|
121
124
|
outputSchema?: IOSchema;
|
|
125
|
+
/** Set by `defineSandboxEnvironment` — marks an environment build so the
|
|
126
|
+
* server skips the /factory mount for its runs (#13). See
|
|
127
|
+
* `WorkflowMetadata.environmentBuild`. */
|
|
128
|
+
environmentBuild?: boolean;
|
|
122
129
|
/** Factory slug. Defaults to `"default"`. */
|
|
123
130
|
factorySlug?: string;
|
|
124
131
|
}
|
|
@@ -139,6 +146,10 @@ export interface InvokeWorkflowOptions {
|
|
|
139
146
|
* vars after brokering. Replaces the template-level placeholders for
|
|
140
147
|
* this run only — registered metadata is not mutated. */
|
|
141
148
|
placeholders?: Record<string, string>;
|
|
149
|
+
/** Per-invocation machine-size override of the template's `resources.size`.
|
|
150
|
+
* `small` (default) | `medium` | `large`; omit → the template default,
|
|
151
|
+
* else `small`. Honoured on Vercel (→ vCPUs); E2B ignores it. */
|
|
152
|
+
size?: SandboxSize;
|
|
142
153
|
/** Explicit parent run id. Pass `null` to suppress ambient RUN_ID auto-detection. */
|
|
143
154
|
parentRunId?: string | null;
|
|
144
155
|
/** Agent loop inside the parent run that caused this invoke, when applicable. */
|
|
@@ -179,6 +190,56 @@ export interface ListTemplatesOptions {
|
|
|
179
190
|
factorySlug?: string;
|
|
180
191
|
}
|
|
181
192
|
|
|
193
|
+
/** A human member of your team — the people an agent (or you) can @-flag. */
|
|
194
|
+
export interface TeamMember {
|
|
195
|
+
/** Membership row id. */
|
|
196
|
+
id: string;
|
|
197
|
+
/** The user id — what you pass to `createMentions({ mentionedUserIds })`. */
|
|
198
|
+
userId: string;
|
|
199
|
+
role: string;
|
|
200
|
+
email: string;
|
|
201
|
+
name: string;
|
|
202
|
+
joinedAt: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** A "you were flagged" ping, persisted server-side so it reaches the
|
|
206
|
+
* mentioned teammate in their Workbench. */
|
|
207
|
+
export interface Mention {
|
|
208
|
+
id: string;
|
|
209
|
+
factoryId: string;
|
|
210
|
+
mentionedUserId: string;
|
|
211
|
+
/** Who flagged: 'user' | 'api_key' | 'run' | 'system'. */
|
|
212
|
+
actorKind: string;
|
|
213
|
+
actorId: string | null;
|
|
214
|
+
actorLabel: string | null;
|
|
215
|
+
/** Where it lives: 'doc' | 'comment' | 'plan' | 'run'. */
|
|
216
|
+
contextKind: string;
|
|
217
|
+
contextPath: string | null;
|
|
218
|
+
/** Ready-made relative dashboard URL the Workbench card links to. */
|
|
219
|
+
contextUrl: string | null;
|
|
220
|
+
text: string;
|
|
221
|
+
runId: string | null;
|
|
222
|
+
seenAt: string | null;
|
|
223
|
+
resolvedAt: string | null;
|
|
224
|
+
createdAt: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface CreateMentionsInput {
|
|
228
|
+
/** Team-member user ids to flag (1–20). Discover them via `listMembers()`.
|
|
229
|
+
* Non-members are dropped server-side. */
|
|
230
|
+
mentionedUserIds: string[];
|
|
231
|
+
/** The flag message shown in the teammate's Workbench. */
|
|
232
|
+
text: string;
|
|
233
|
+
contextKind: "doc" | "comment" | "plan" | "run";
|
|
234
|
+
/** Factory-relative file path or comment thread id, when applicable. */
|
|
235
|
+
contextPath?: string;
|
|
236
|
+
/** Ready-made relative dashboard URL the Workbench card links to (e.g.
|
|
237
|
+
* `/factories/<slug>/files/view?path=<plan>`). */
|
|
238
|
+
contextUrl?: string;
|
|
239
|
+
runId?: string;
|
|
240
|
+
factorySlug?: string;
|
|
241
|
+
}
|
|
242
|
+
|
|
182
243
|
export interface CreateFactoryInput {
|
|
183
244
|
slug: string;
|
|
184
245
|
name: string;
|
|
@@ -661,6 +722,7 @@ export class AgentComposeClient {
|
|
|
661
722
|
...(opts?.snapshots !== undefined ? { snapshots: opts.snapshots } : {}),
|
|
662
723
|
...(opts?.networkPolicy !== undefined ? { networkPolicy: opts.networkPolicy } : {}),
|
|
663
724
|
...(opts?.placeholders !== undefined ? { placeholders: opts.placeholders } : {}),
|
|
725
|
+
...(opts?.size !== undefined ? { size: opts.size } : {}),
|
|
664
726
|
...(parentRunId ? { parentRunId } : {}),
|
|
665
727
|
...(opts?.agentId ? { agentId: opts.agentId } : {}),
|
|
666
728
|
},
|
|
@@ -1014,6 +1076,36 @@ export class AgentComposeClient {
|
|
|
1014
1076
|
);
|
|
1015
1077
|
}
|
|
1016
1078
|
|
|
1079
|
+
/** List the human members of your team — the people you (or an agent) can
|
|
1080
|
+
* @-flag with `createMentions`. Each row's `userId` is what
|
|
1081
|
+
* `mentionedUserIds` expects. */
|
|
1082
|
+
async listMembers(): Promise<TeamMember[]> {
|
|
1083
|
+
const body = await this.fetch<{ members: TeamMember[] }>("/api/v1/team/members");
|
|
1084
|
+
return body.members;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
/** Flag one or more teammates — a durable ping that lands in their factory
|
|
1088
|
+
* Workbench. Use from an agent (e.g. a remediation plan that needs a human
|
|
1089
|
+
* to rotate a secret) or any team automation. Resolve `mentionedUserIds`
|
|
1090
|
+
* via `listMembers()`. When run inside a sandbox the run-callback token is
|
|
1091
|
+
* forwarded so the ping is attributed to the run ("flagged by <workflow>"). */
|
|
1092
|
+
async createMentions(input: CreateMentionsInput): Promise<Mention[]> {
|
|
1093
|
+
const factorySlug = input.factorySlug
|
|
1094
|
+
?? (typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_FACTORY : undefined)
|
|
1095
|
+
?? DEFAULT_FACTORY;
|
|
1096
|
+
const runToken = typeof process !== "undefined" ? process.env?.AGENT_COMPOSE_RUN_TOKEN : undefined;
|
|
1097
|
+
const { factorySlug: _omit, ...payload } = input;
|
|
1098
|
+
const body = await this.fetch<{ mentions: Mention[] }>(
|
|
1099
|
+
`/api/v1/factories/${encodeURIComponent(factorySlug)}/mentions`,
|
|
1100
|
+
{
|
|
1101
|
+
method: "POST",
|
|
1102
|
+
body: payload,
|
|
1103
|
+
...(runToken ? { headers: { "x-run-token": runToken } } : {}),
|
|
1104
|
+
},
|
|
1105
|
+
);
|
|
1106
|
+
return body.mentions;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1017
1109
|
/** List events ingested into a factory, newest first. Supports
|
|
1018
1110
|
* case-insensitive substring filter (`name`) and timestamp-cursor
|
|
1019
1111
|
* pagination (`before`). Returns `{ events, has_more }` — the
|
|
@@ -1153,6 +1245,35 @@ export class AgentComposeClient {
|
|
|
1153
1245
|
return this.fetch(templatePath(factorySlug, workflowName, "secrets", key), { method: "DELETE" });
|
|
1154
1246
|
}
|
|
1155
1247
|
|
|
1248
|
+
// ── Factory-level secrets (ADR-0014) ────────────────────────────────────────
|
|
1249
|
+
// The inherited tier: a factory secret is visible to EVERY workflow in the
|
|
1250
|
+
// factory; a workflow secret of the same key overrides it. Values are stored
|
|
1251
|
+
// in GCP Secret Manager; never returned by reads.
|
|
1252
|
+
|
|
1253
|
+
/** Create or update a factory-level secret. */
|
|
1254
|
+
setFactorySecret(key: string, value: string, opts?: SecretOptions): Promise<SetSecretResult> {
|
|
1255
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1256
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`, {
|
|
1257
|
+
method: "POST",
|
|
1258
|
+
body: { key, value },
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
/** List factory-level secret keys (metadata only — values are never returned). */
|
|
1263
|
+
async listFactorySecrets(opts?: SecretOptions): Promise<SecretListEntry[]> {
|
|
1264
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1265
|
+
const body = await this.fetch<{ secrets: Array<{ secretKey: string; createdAt: string; updatedAt: string }> }>(
|
|
1266
|
+
`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets`,
|
|
1267
|
+
);
|
|
1268
|
+
return body.secrets.map(s => ({ key: s.secretKey, createdAt: s.createdAt, updatedAt: s.updatedAt }));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
/** Delete a factory-level secret. */
|
|
1272
|
+
deleteFactorySecret(key: string, opts?: SecretOptions): Promise<void> {
|
|
1273
|
+
const factorySlug = opts?.factorySlug ?? DEFAULT_FACTORY;
|
|
1274
|
+
return this.fetch(`/api/v1/factories/${encodeURIComponent(factorySlug)}/secrets/${encodeURIComponent(key)}`, { method: "DELETE" });
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1156
1277
|
// ── API keys ───────────────────────────────────────────────────────────────
|
|
1157
1278
|
// Both endpoints require an admin-scoped key as the bearer token.
|
|
1158
1279
|
|
package/src/index.ts
CHANGED
|
@@ -73,6 +73,7 @@ export {
|
|
|
73
73
|
Verdict,
|
|
74
74
|
runProcessorChain,
|
|
75
75
|
denyTools,
|
|
76
|
+
humanApproval,
|
|
76
77
|
requireScope,
|
|
77
78
|
redactPattern,
|
|
78
79
|
} from "./processors/index.js";
|
|
@@ -112,6 +113,7 @@ export type {
|
|
|
112
113
|
CreateFactoryInput, UpdateFactoryInput,
|
|
113
114
|
SecretOptions, SetSecretResult, SecretListEntry,
|
|
114
115
|
CreateApiKeyInput, StreamRunLogsOptions,
|
|
116
|
+
TeamMember, Mention, CreateMentionsInput,
|
|
115
117
|
EventSubjectType, EventRow, ReportEventInput, ListEventsOptions, ListEventsResult,
|
|
116
118
|
RunLogLine, ListRunLogsOptions,
|
|
117
119
|
RegisteredRuntime, RunState, RunStatus, FactoryRow, SnapshotListEntry, SnapshotListResponse,
|
|
@@ -178,16 +180,20 @@ export type { RunEvent } from "./types/events.js";
|
|
|
178
180
|
|
|
179
181
|
// Sandbox providers
|
|
180
182
|
export { createSandbox, reconnectSandbox, killAllSandboxes, killSandboxById,
|
|
181
|
-
getSandboxQuotas, listOwnedSandboxes, deleteSandboxSnapshot,
|
|
183
|
+
getSandboxQuotas, listOwnedSandboxes, deleteSandboxSnapshot, snapshotResolves,
|
|
182
184
|
makeSandboxProvider, makeDesktopSandboxProvider,
|
|
183
|
-
parseSseExecStream, AGENT_COMPOSE_TAG
|
|
185
|
+
parseSseExecStream, AGENT_COMPOSE_TAG,
|
|
186
|
+
SANDBOX_VCPUS, DEFAULT_SANDBOX_SIZE, E2B_TEMPLATE_SIZES,
|
|
187
|
+
isE2bSupportedSize, e2bMachineSpec, e2bBaseTemplate, e2bAgentEnvTemplate,
|
|
188
|
+
isPlatformE2bTemplateAlias } from "./sandbox.js";
|
|
184
189
|
export { SandboxUnavailableError, SANDBOX_UNAVAILABLE_PREFIX } from "./sandbox-errors.js";
|
|
185
190
|
export type {
|
|
186
191
|
SandboxCreateOpts, SandboxNetworkPolicy, SandboxNetworkHeaderTransform,
|
|
187
192
|
SandboxNetworkAllowRule, SandboxNetworkSubnetPolicy, SandboxProviderName,
|
|
188
|
-
SandboxQuotaResult, OwnedSandboxResult, OwnedSandbox,
|
|
193
|
+
SandboxQuotaResult, OwnedSandboxResult, OwnedSandbox, SandboxSize,
|
|
189
194
|
ParseSseExecStreamOptions, SandboxCommandRunOptions, SandboxCommandResult,
|
|
190
195
|
} from "./sandbox.js";
|
|
196
|
+
export type { SandboxResources } from "./types/workflow-metadata.js";
|
|
191
197
|
|
|
192
198
|
// Workflow engine
|
|
193
199
|
export { runWorkflow, WorkflowError, EngineError, classifyError, parseNameVersion } from "./workflows/engine.js";
|
|
@@ -249,7 +255,11 @@ export {
|
|
|
249
255
|
} from "./pause/errors.js";
|
|
250
256
|
export type { PauseErrorCode } from "./pause/errors.js";
|
|
251
257
|
export type { PauseRequest } from "./pause/pause-core.js";
|
|
252
|
-
export type {
|
|
258
|
+
export type { WaitForEventRequest } from "./pause/wrappers.js";
|
|
259
|
+
// `agentc pause` writes a local pause-request marker the agent loop consumes
|
|
260
|
+
// (the snapshot-release bridge); the CLI imports the writer.
|
|
261
|
+
export { writeLocalPauseRequest, consumeLocalPauseRequest } from "./agent/local-pause-request.js";
|
|
262
|
+
export type { LocalPauseRequest, PauseOption } from "./agent/local-pause-request.js";
|
|
253
263
|
export type {
|
|
254
264
|
StepRequest,
|
|
255
265
|
StepResult,
|
|
@@ -265,5 +275,7 @@ export { agentLoop, parseAgentStatus, DEFAULT_CLAUDE_MODEL } from "./agent/agent
|
|
|
265
275
|
export type { AgentLifecycleEvent, AgentLoopOpts, AgentLoopResult } from "./agent/agent-loop.js";
|
|
266
276
|
export { agent } from "./agent/run-agent.js";
|
|
267
277
|
export type { AgentOpts } from "./agent/run-agent.js";
|
|
278
|
+
export { AGENT_COMPOSE_MANUAL, buildAgentContextDoc, writeAgentContext } from "./agent/agent-context.js";
|
|
279
|
+
export type { AgentConnectorInfo } from "./agent/agent-context.js";
|
|
268
280
|
export { AgentMessageSchema, parseAgentResponse } from "./agent/protocol.js";
|
|
269
281
|
export { importSourceModule, TMP_DIR, LATEST_VERSION } from "./utils/source-loader.js";
|
package/src/pause/checkpoint.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Disk-backed memoise across pause-resume — the engine behind durable
|
|
3
|
+
* `ctx.step(name, fn)` (ADR-0012).
|
|
3
4
|
*
|
|
4
5
|
* First call: `fn()` runs, the result is atomically written to
|
|
5
6
|
* `/tmp/wf/state/checkpoints/<name>.json`. On any subsequent invocation
|
|
@@ -16,29 +17,47 @@
|
|
|
16
17
|
* memoisation is in-sandbox only and would silently retry on a sandbox
|
|
17
18
|
* recreation that lost the checkpoint file.
|
|
18
19
|
*
|
|
19
|
-
*
|
|
20
|
+
* INTERNAL only — there is no public `ctx.checkpoint`. The durable
|
|
21
|
+
* `ctx.step` wires `scopedMemoize("step<idx>")` and reports a "restored"
|
|
22
|
+
* sub-step on a cache hit. See ADR-0012.
|
|
20
23
|
*/
|
|
21
24
|
|
|
22
25
|
import { assertSafeCheckpointName, readCheckpoint, writeCheckpoint } from "./state-dir.js";
|
|
23
26
|
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
27
|
+
/** Result of a memoise call — `restored` distinguishes a disk hit (fn was
|
|
28
|
+
* NOT run, value read from a prior subprocess) from a fresh run (fn ran,
|
|
29
|
+
* value just written). The durable `ctx.step` maps `restored` onto the
|
|
30
|
+
* duration-0 "restored" sub-step status. */
|
|
31
|
+
export interface MemoizeResult<T> {
|
|
32
|
+
value: T;
|
|
33
|
+
restored: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Read-or-run-and-write helper reporting whether the value was restored
|
|
37
|
+
* from disk. `name` is validated by the state-dir layer (rejects
|
|
38
|
+
* path-escape attempts); `fn` is awaited if it returns a Promise so sync
|
|
39
|
+
* and async callers compose identically.
|
|
40
|
+
*
|
|
41
|
+
* A pause inside `fn` (PauseSignal) propagates BEFORE any write — a
|
|
42
|
+
* partially-completed body is never memoised, so the resume re-runs it. */
|
|
43
|
+
export async function memoize<T>(name: string, fn: () => Promise<T> | T): Promise<MemoizeResult<T>> {
|
|
28
44
|
const existing = await readCheckpoint<T>(name);
|
|
29
|
-
if (existing.found) return existing.value;
|
|
45
|
+
if (existing.found) return { value: existing.value, restored: true };
|
|
30
46
|
const value = await fn();
|
|
31
47
|
await writeCheckpoint(name, value);
|
|
32
|
-
return value;
|
|
48
|
+
return { value, restored: false };
|
|
33
49
|
}
|
|
34
50
|
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
51
|
+
/** A memoise function bound to a runner-owned scope. */
|
|
52
|
+
export type ScopedMemoize = <T>(name: string, fn: () => Promise<T> | T) => Promise<MemoizeResult<T>>;
|
|
53
|
+
|
|
54
|
+
/** Bind memoise names to a runner-owned namespace (e.g. `step<idx>`). The
|
|
55
|
+
* caller's `name` is still validated independently so a scope prefix
|
|
56
|
+
* cannot turn an empty or path-escaping name into a valid filename. */
|
|
57
|
+
export function scopedMemoize(scope: string): ScopedMemoize {
|
|
39
58
|
assertSafeCheckpointName(scope);
|
|
40
|
-
return async <T>(name: string, fn: () => Promise<T> | T): Promise<T
|
|
59
|
+
return async <T>(name: string, fn: () => Promise<T> | T): Promise<MemoizeResult<T>> => {
|
|
41
60
|
assertSafeCheckpointName(name);
|
|
42
|
-
return
|
|
61
|
+
return memoize(`${scope}.${name}`, fn);
|
|
43
62
|
};
|
|
44
63
|
}
|
package/src/pause/manager.ts
CHANGED
|
@@ -44,7 +44,7 @@ const RunningAgentLoopStateSchema = z.object({
|
|
|
44
44
|
blockerStreak: z.object({ key: z.string(), count: z.number().int().nonnegative() }).nullable(),
|
|
45
45
|
// PR 7: a human-requested steer-pause intent, persisted so it survives a
|
|
46
46
|
// resume and the loop re-issues the boundary pause on re-entry.
|
|
47
|
-
pendingSteerPause: z.object({ reason: z.string(), correlationKey: z.string().nullable(), at: z.number().int() }).nullable().default(null),
|
|
47
|
+
pendingSteerPause: z.object({ reason: z.string(), correlationKey: z.string().nullable(), at: z.number().int(), payload: z.record(z.string(), z.unknown()).optional() }).nullable().default(null),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
50
|
const SettledAgentLoopStateSchema = z.object({
|
|
@@ -76,7 +76,7 @@ export interface AgentLoopProgressState {
|
|
|
76
76
|
blockerStreak: { key: string; count: number } | null;
|
|
77
77
|
/** PR 7 steer-pause intent — see RunningAgentLoopStateSchema. Optional so the
|
|
78
78
|
* loop can omit it until it wires steer (the schema defaults it to null). */
|
|
79
|
-
pendingSteerPause?: { reason: string; correlationKey: string | null; at: number } | null;
|
|
79
|
+
pendingSteerPause?: { reason: string; correlationKey: string | null; at: number; payload?: Record<string, unknown> } | null;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export interface SettledAgentLoopResult<TResponse = unknown> {
|
package/src/pause/pause-core.ts
CHANGED
|
@@ -94,6 +94,41 @@ export function isPauseSignal(err: unknown): err is PauseSignal {
|
|
|
94
94
|
return typeof err === "object" && err !== null && (err as Record<symbol, unknown>)[PAUSE_SIGNAL_BRAND] === true;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* The run's pause boundary. The runner (`run-agent` `steerPause`) closes a
|
|
99
|
+
* `corePause` over the ambient `runId`/`stepIndex`; the agent loop / runtime
|
|
100
|
+
* supplies the per-iteration `agentScope` so the pauseId stays stable across
|
|
101
|
+
* the loop's iteration-skipping on resume. Absent outside a workflow step
|
|
102
|
+
* (local / non-sandbox callers) — see `boundProcessorPause`.
|
|
103
|
+
*/
|
|
104
|
+
export type BoundaryPauseFn = <T = unknown>(
|
|
105
|
+
req: PauseRequest<T>,
|
|
106
|
+
agentScope: { agentId: string; iteration: number },
|
|
107
|
+
) => Promise<T>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build the `ProcessorContext.pause` callable for a given hook scope: binds the
|
|
111
|
+
* boundary to this hook's `{ agentId, iteration }` so a processor calling
|
|
112
|
+
* `ctx.pause(req)` gets a deterministic, resume-stable pauseId. When no boundary
|
|
113
|
+
* is wired (local tests, non-sandbox callers), the returned fn rejects loudly
|
|
114
|
+
* rather than silently no-op'ing — pause genuinely cannot work without the
|
|
115
|
+
* runner's snapshot/Temporal machinery.
|
|
116
|
+
*/
|
|
117
|
+
export function boundProcessorPause(
|
|
118
|
+
boundary: BoundaryPauseFn | undefined,
|
|
119
|
+
scope: { agentId: string; iteration: number },
|
|
120
|
+
): <T = unknown>(req: PauseRequest<T>) => Promise<T> {
|
|
121
|
+
return <T = unknown>(req: PauseRequest<T>): Promise<T> => {
|
|
122
|
+
if (!boundary) {
|
|
123
|
+
return Promise.reject(new PauseRequestError(
|
|
124
|
+
"ctx.pause is unavailable here: no pause boundary is wired (a processor can " +
|
|
125
|
+
"only pause inside a sandboxed workflow step, not in a local/unit-test run)",
|
|
126
|
+
));
|
|
127
|
+
}
|
|
128
|
+
return boundary(req, scope);
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
97
132
|
// ── Deterministic pauseId ───────────────────────────────────────────────────
|
|
98
133
|
|
|
99
134
|
/** Fixed namespace for agent-compose pause ids (UUIDv5). Constant — must
|
package/src/pause/state-dir.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* agent-<agentInstanceId>.json ← agent loop state (iteration, messages, processor cursor)
|
|
8
8
|
* runtime-<agentInstanceId>.json ← runtime-private blob (opaque to the loop)
|
|
9
|
-
* checkpoints/<name>.json ←
|
|
9
|
+
* checkpoints/<name>.json ← durable `ctx.step(name, fn)` memoised values (keyed `step<idx>.<name>`)
|
|
10
10
|
* pauses/<pauseId>.json ← pause records (request + resume payload once available)
|
|
11
11
|
*
|
|
12
12
|
* Atomic writes (write-tmp → fsync → rename) so a mid-write `sandbox.snapshot()`
|
|
@@ -50,7 +50,7 @@ const pausePath = (pauseId: string) => join(pausesDir(), `${pause
|
|
|
50
50
|
|
|
51
51
|
/** `name` is interpolated into a filename; reject anything outside a
|
|
52
52
|
* conservative slug. Prevents a workflow author from writing
|
|
53
|
-
* `ctx.
|
|
53
|
+
* `ctx.step("../../etc/passwd", fn)` and escaping the dir.
|
|
54
54
|
*
|
|
55
55
|
* First character must be alphanumeric or `_` so dotfile names (`.foo`)
|
|
56
56
|
* and parent-directory tokens (`.`, `..`) are rejected regardless of
|
package/src/pause/wrappers.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The
|
|
2
|
+
* The two opinionated pause wrappers (ADR-0006 §"SDK surface"), each a thin
|
|
3
3
|
* closure over `ctx.pause`:
|
|
4
4
|
*
|
|
5
|
-
* - `requestDecision` — pause for a typed human/agent decision (schema required).
|
|
6
5
|
* - `sleep` — a lightweight timed pause; resolves on its own TTL,
|
|
7
6
|
* skips the snapshot, returns void.
|
|
8
7
|
* - `waitForEvent` — pause until an event resumes by correlation key.
|
|
9
8
|
*
|
|
9
|
+
* A typed human/agent decision is NOT a wrapper — it's a plain `ctx.pause`
|
|
10
|
+
* with a `schema` (and `payload.options` for the dashboard's answer UI). The
|
|
11
|
+
* pause primitive carries the reason (the ask) and the resume value (the
|
|
12
|
+
* resolution); there is no separate `requestDecision`.
|
|
13
|
+
*
|
|
10
14
|
* Built from a `PauseFn` so the wrapper logic lives in one place and the step
|
|
11
15
|
* runner just spreads them onto the context next to `pause`.
|
|
12
16
|
*/
|
|
@@ -19,18 +23,10 @@ import type { PauseRequest } from "./pause-core.js";
|
|
|
19
23
|
export type PauseFn = <T = unknown>(req: PauseRequest<T>) => Promise<T>;
|
|
20
24
|
|
|
21
25
|
/** Internal: pause with an explicit wire `kind`. The wrappers stamp
|
|
22
|
-
* `
|
|
26
|
+
* `sleep`/`event` through this; the public `ctx.pause` is always
|
|
23
27
|
* `custom` and never exposes it. */
|
|
24
28
|
export type KindedPauseFn = <T = unknown>(req: PauseRequest<T>, kind: StepPauseRequest["kind"]) => Promise<T>;
|
|
25
29
|
|
|
26
|
-
export interface RequestDecisionRequest<T> {
|
|
27
|
-
reason: string;
|
|
28
|
-
payload?: Record<string, unknown>;
|
|
29
|
-
/** Required — a decision is always validated against a shape. */
|
|
30
|
-
schema: z.ZodType<T>;
|
|
31
|
-
ttlMs?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
30
|
export interface WaitForEventRequest<T> {
|
|
35
31
|
reason: string;
|
|
36
32
|
/** Required — the by-key resume route targets this. */
|
|
@@ -40,22 +36,12 @@ export interface WaitForEventRequest<T> {
|
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
export interface PauseWrappers {
|
|
43
|
-
requestDecision<T>(req: RequestDecisionRequest<T>): Promise<T>;
|
|
44
39
|
sleep(durationMs: number): Promise<void>;
|
|
45
40
|
waitForEvent<T = unknown>(req: WaitForEventRequest<T>): Promise<T>;
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
export function buildPauseWrappers(pause: KindedPauseFn): PauseWrappers {
|
|
49
44
|
return {
|
|
50
|
-
requestDecision<T>(req: RequestDecisionRequest<T>): Promise<T> {
|
|
51
|
-
return pause<T>({
|
|
52
|
-
reason: req.reason,
|
|
53
|
-
schema: req.schema,
|
|
54
|
-
...(req.payload !== undefined ? { payload: req.payload } : {}),
|
|
55
|
-
...(req.ttlMs !== undefined ? { ttlMs: req.ttlMs } : {}),
|
|
56
|
-
}, "decision");
|
|
57
|
-
},
|
|
58
|
-
|
|
59
45
|
// Resolves on its OWN ttl — there is no external resumer for a sleep, so
|
|
60
46
|
// onExpiry resolves (not throws) with void. snapshot:false keeps it cheap.
|
|
61
47
|
sleep(durationMs: number): Promise<void> {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* exist as load-bearing examples and as defaults for common policies.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import
|
|
7
|
+
import { z } from "zod";
|
|
8
|
+
import type { Processor, ToolCall } from "./processor.js";
|
|
8
9
|
import { Verdict } from "./processor.js";
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -27,6 +28,48 @@ export function denyTools(names: readonly string[]): Processor {
|
|
|
27
28
|
};
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/** Resume payload a reviewer sends back to a `humanApproval` pause. */
|
|
32
|
+
const ApprovalDecision = z.object({
|
|
33
|
+
approved: z.boolean(),
|
|
34
|
+
reason: z.string().optional(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Pause the run for HUMAN APPROVAL before a matching tool executes — the
|
|
39
|
+
* human-in-the-loop pre-tool gate (ADR-0006 `ctx.pause`). Over an ACP runtime
|
|
40
|
+
* this is exactly the `session/request_permission` path: the CLI asks to run a
|
|
41
|
+
* tool, the gate runs here, and `ctx.pause` snapshots the workflow and waits
|
|
42
|
+
* durably until a reviewer resolves the pause with `{ approved, reason? }`.
|
|
43
|
+
* Approve → the tool runs; deny → the reason is returned to the model as the
|
|
44
|
+
* tool result and the loop continues.
|
|
45
|
+
*
|
|
46
|
+
* tools? — only these tool names require approval (default: EVERY tool call).
|
|
47
|
+
* reason? — build the human-facing prompt from the call (default names the tool).
|
|
48
|
+
*
|
|
49
|
+
* Dormant on runtimes without pre-tool gating; active on those that wire
|
|
50
|
+
* `processToolCall` (the ACP CLI runtimes, the Claude pre-tool hook, Vercel).
|
|
51
|
+
*/
|
|
52
|
+
export function humanApproval(opts?: {
|
|
53
|
+
tools?: readonly string[];
|
|
54
|
+
reason?: (call: ToolCall) => string;
|
|
55
|
+
}): Processor {
|
|
56
|
+
const gated = opts?.tools ? new Set(opts.tools) : null;
|
|
57
|
+
return {
|
|
58
|
+
name: "humanApproval",
|
|
59
|
+
async processToolCall(call, ctx) {
|
|
60
|
+
if (gated && !gated.has(call.toolName)) return Verdict.continue(call);
|
|
61
|
+
const decision = await ctx.pause({
|
|
62
|
+
reason: opts?.reason?.(call) ?? `Approve tool call: ${call.toolName}`,
|
|
63
|
+
payload: { tool: call.toolName, input: call.toolInput, toolUseId: call.toolUseId },
|
|
64
|
+
schema: ApprovalDecision,
|
|
65
|
+
});
|
|
66
|
+
return decision.approved
|
|
67
|
+
? Verdict.continue(call)
|
|
68
|
+
: Verdict.deny(decision.reason ?? `tool "${call.toolName}" denied by human reviewer`);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
30
73
|
/**
|
|
31
74
|
* Require the calling API key to carry every scope in `required`. Aborts the
|
|
32
75
|
* agent loop with a clear reason if any are missing — defence-in-depth on
|
package/src/processors/index.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
|
|
31
31
|
import type { AgentMessage } from "../types/protocol.js";
|
|
32
32
|
import type { RequestContext } from "../request-context/request-context.js";
|
|
33
|
+
import type { PauseRequest } from "../pause/pause-core.js";
|
|
33
34
|
|
|
34
35
|
/** Proposed tool call. Mirrors the relevant fields of AgentMessageToolUse but
|
|
35
36
|
* lives as its own type so runtime adapters (candidate #1) can populate it
|
|
@@ -53,6 +54,18 @@ export interface ProcessorContext {
|
|
|
53
54
|
agentId: string;
|
|
54
55
|
/** Iteration of the agent loop (1-based). */
|
|
55
56
|
iteration: number;
|
|
57
|
+
/**
|
|
58
|
+
* Pause the run from inside a processor — the human-approval primitive for a
|
|
59
|
+
* pre-tool-use gate (ADR-0006 §"reachable from every user code path";
|
|
60
|
+
* ADR-0020 the ACP `session/request_permission` path). On the fresh pass it
|
|
61
|
+
* throws `PauseSignal` (internal control flow) so the workflow snapshots and
|
|
62
|
+
* waits durably; on step re-entry after resolution it returns the resume
|
|
63
|
+
* payload (validated against `req.schema` if given). The agent loop / runtime
|
|
64
|
+
* wires it to the run's pause boundary with a deterministic, resume-stable
|
|
65
|
+
* pauseId (keyed on this hook's agentId + iteration). Calling it outside a
|
|
66
|
+
* sandboxed workflow step throws — pause requires the runner's pause boundary.
|
|
67
|
+
*/
|
|
68
|
+
pause<T = unknown>(req: PauseRequest<T>): Promise<T>;
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
/**
|