@agent-compose/sdk 0.5.9 → 0.7.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 (49) hide show
  1. package/dist/agent/agent-context.d.ts +1 -1
  2. package/dist/agent/agent-loop.d.ts +0 -8
  3. package/dist/agent/pause-client.d.ts +50 -0
  4. package/dist/index.d.ts +15 -6
  5. package/dist/index.js +537 -124
  6. package/dist/processors/ask-human.d.ts +30 -0
  7. package/dist/processors/ask-human.test.d.ts +1 -0
  8. package/dist/processors/gate-pause.d.ts +46 -0
  9. package/dist/processors/gate-pause.test.d.ts +1 -0
  10. package/dist/processors/index.d.ts +3 -0
  11. package/dist/runtimes/_cli-agent.d.ts +9 -0
  12. package/dist/runtimes/cursor.d.ts +9 -0
  13. package/dist/runtimes/droid.d.ts +9 -0
  14. package/dist/runtimes/openai-desktop.js +522 -122
  15. package/dist/runtimes/opencode.d.ts +25 -0
  16. package/dist/runtimes/vercel.js +11 -1
  17. package/dist/step-invocation/__tests__/background-invoker.test.d.ts +1 -0
  18. package/dist/step-invocation/index.d.ts +2 -1
  19. package/dist/step-invocation/invoker.d.ts +49 -0
  20. package/dist/step-invocation/protocol.d.ts +8 -0
  21. package/dist/types/runtime.d.ts +7 -0
  22. package/dist/types/sandbox.d.ts +54 -0
  23. package/dist/types/workflow.d.ts +12 -0
  24. package/dist/utils/errors.d.ts +9 -1
  25. package/package.json +1 -1
  26. package/src/agent/agent-context.ts +23 -16
  27. package/src/agent/agent-loop.ts +13 -33
  28. package/src/agent/pause-client.ts +108 -0
  29. package/src/agent/run-agent.ts +16 -7
  30. package/src/index.ts +27 -4
  31. package/src/processors/ask-human.ts +136 -0
  32. package/src/processors/gate-pause.ts +94 -0
  33. package/src/processors/index.ts +11 -0
  34. package/src/runtimes/_cli-agent.ts +13 -5
  35. package/src/runtimes/claude.ts +10 -6
  36. package/src/runtimes/cursor.ts +59 -0
  37. package/src/runtimes/droid.ts +63 -0
  38. package/src/runtimes/opencode.ts +61 -0
  39. package/src/sandbox.ts +78 -3
  40. package/src/step-invocation/index.ts +2 -1
  41. package/src/step-invocation/invoker.ts +359 -86
  42. package/src/step-invocation/protocol.ts +11 -0
  43. package/src/types/runtime.ts +7 -0
  44. package/src/types/sandbox.ts +53 -0
  45. package/src/types/workflow.ts +12 -0
  46. package/src/utils/errors.ts +19 -2
  47. package/dist/agent/local-pause-request.d.ts +0 -49
  48. package/src/agent/local-pause-request.ts +0 -90
  49. /package/dist/agent/{local-pause-request.test.d.ts → pause-client.test.d.ts} +0 -0
@@ -1,4 +1,21 @@
1
- /** Convert any thrown value to a string message. */
1
+ /** Convert any thrown value to a string message, INCLUDING its `.cause` chain.
2
+ *
3
+ * Many wrapped errors carry the real reason on `.cause` and only a generic
4
+ * summary on `.message` — the Temporal SDK's "Failed to start Workflow" is the
5
+ * canonical example (its `.cause` is the actual gRPC rejection, e.g. "search
6
+ * attribute X is not defined"). Returning `.message` alone swallowed that, so
7
+ * failures surfaced as opaque one-liners. Walk the chain and join the messages
8
+ * so the root cause is always visible. Cycle-guarded against self-referential
9
+ * `cause` links. */
2
10
  export function formatError(err: unknown): string {
3
- return err instanceof Error ? err.message : String(err);
11
+ if (!(err instanceof Error)) return String(err);
12
+ const parts: string[] = [err.message];
13
+ const seen = new Set<unknown>([err]);
14
+ let cause: unknown = (err as { cause?: unknown }).cause;
15
+ while (cause != null && !seen.has(cause)) {
16
+ seen.add(cause);
17
+ parts.push(cause instanceof Error ? cause.message : String(cause));
18
+ cause = cause instanceof Error ? (cause as { cause?: unknown }).cause : undefined;
19
+ }
20
+ return parts.join(": ");
4
21
  }
@@ -1,49 +0,0 @@
1
- /**
2
- * Local pause-request marker — the in-sandbox bridge from `agentc pause` to
3
- * the agent loop's snapshot-release pause boundary.
4
- *
5
- * `agentc pause` runs as a grandchild subprocess of the runner (the agent CLI
6
- * shells out to it). It cannot throw a `PauseSignal` into the loop and the
7
- * in-memory steer flag (`signalSteerPending`) lives in a different process, so
8
- * the only reliable channel is the shared sandbox filesystem. The CLI writes a
9
- * durable marker here; the agent loop consumes it at the end of the turn
10
- * (alongside the `needs_input` self-pause) and stages a real `ctx.pause` the
11
- * next boundary takes — which snapshots the sandbox, releases the activity
12
- * (compute stops), and parks the workflow. On resume the human's answer is
13
- * delivered as the agent's next user turn.
14
- *
15
- * This is the ONLY place the marker path + shape are defined — both the CLI
16
- * (writer) and the SDK loop (reader) import it, so the two halves can never
17
- * drift. Like the rest of the state-dir, the layout is a wire protocol between
18
- * the runner and the next subprocess invocation (ADR-0006). The marker is
19
- * consumed BEFORE the snapshot, so it never needs to survive a pause.
20
- *
21
- * Scoped by `agentId` so concurrent `agent()` calls sharing one sandbox each
22
- * see only their own request. When the id is unavailable (older agent env that
23
- * doesn't inject `AGENT_COMPOSE_AGENT_ID`) both sides fall back to a single
24
- * unscoped slot — correct for the common single-agent step, and the only case
25
- * where an unscoped marker can be ambiguous (two anonymous agents) is one the
26
- * old block-poll CLI couldn't handle either.
27
- */
28
- /** A choice offered to the human. A bare string is shorthand for
29
- * `{ label, value }` with both equal — exactly what the dashboard's
30
- * `readOptions` accepts. */
31
- export type PauseOption = string | {
32
- label: string;
33
- value: string;
34
- };
35
- /** What `agentc pause` records for the loop to turn into a `ctx.pause`. */
36
- export interface LocalPauseRequest {
37
- /** The question shown to the human (becomes the pause `reason`). */
38
- reason: string;
39
- /** Optional offered choices, rendered as buttons in the dashboard. */
40
- options?: PauseOption[];
41
- }
42
- /** Write the marker atomically (tmp → rename) so the loop never reads a
43
- * partial file mid-write. Called by `agentc pause`. */
44
- export declare function writeLocalPauseRequest(agentId: string | undefined | null, req: LocalPauseRequest): void;
45
- /** Take-once read: returns + deletes this agent's pending pause request, else
46
- * null. Checks the agent-scoped slot first, then the unscoped fallback. The
47
- * loop calls this once per turn; a malformed marker is dropped (deleted +
48
- * null) rather than wedging the loop. */
49
- export declare function consumeLocalPauseRequest(agentId: string | undefined | null): LocalPauseRequest | null;
@@ -1,90 +0,0 @@
1
- /**
2
- * Local pause-request marker — the in-sandbox bridge from `agentc pause` to
3
- * the agent loop's snapshot-release pause boundary.
4
- *
5
- * `agentc pause` runs as a grandchild subprocess of the runner (the agent CLI
6
- * shells out to it). It cannot throw a `PauseSignal` into the loop and the
7
- * in-memory steer flag (`signalSteerPending`) lives in a different process, so
8
- * the only reliable channel is the shared sandbox filesystem. The CLI writes a
9
- * durable marker here; the agent loop consumes it at the end of the turn
10
- * (alongside the `needs_input` self-pause) and stages a real `ctx.pause` the
11
- * next boundary takes — which snapshots the sandbox, releases the activity
12
- * (compute stops), and parks the workflow. On resume the human's answer is
13
- * delivered as the agent's next user turn.
14
- *
15
- * This is the ONLY place the marker path + shape are defined — both the CLI
16
- * (writer) and the SDK loop (reader) import it, so the two halves can never
17
- * drift. Like the rest of the state-dir, the layout is a wire protocol between
18
- * the runner and the next subprocess invocation (ADR-0006). The marker is
19
- * consumed BEFORE the snapshot, so it never needs to survive a pause.
20
- *
21
- * Scoped by `agentId` so concurrent `agent()` calls sharing one sandbox each
22
- * see only their own request. When the id is unavailable (older agent env that
23
- * doesn't inject `AGENT_COMPOSE_AGENT_ID`) both sides fall back to a single
24
- * unscoped slot — correct for the common single-agent step, and the only case
25
- * where an unscoped marker can be ambiguous (two anonymous agents) is one the
26
- * old block-poll CLI couldn't handle either.
27
- */
28
-
29
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
30
- import { join } from "node:path";
31
- import { randomBytes } from "node:crypto";
32
-
33
- import { getStateDir } from "../pause/state-dir.js";
34
-
35
- /** A choice offered to the human. A bare string is shorthand for
36
- * `{ label, value }` with both equal — exactly what the dashboard's
37
- * `readOptions` accepts. */
38
- export type PauseOption = string | { label: string; value: string };
39
-
40
- /** What `agentc pause` records for the loop to turn into a `ctx.pause`. */
41
- export interface LocalPauseRequest {
42
- /** The question shown to the human (becomes the pause `reason`). */
43
- reason: string;
44
- /** Optional offered choices, rendered as buttons in the dashboard. */
45
- options?: PauseOption[];
46
- }
47
-
48
- const UNSCOPED = "_unscoped";
49
-
50
- const requestsDir = () => join(getStateDir(), "pause-requests");
51
- const markerPath = (agentId: string | undefined | null) =>
52
- join(requestsDir(), `${slug(agentId) || UNSCOPED}.json`);
53
-
54
- /** Keep the agentId filename-safe. Agent ids are `step<idx>-agent-<n>` shaped,
55
- * but defend against anything exotic so the marker can never escape the dir. */
56
- function slug(agentId: string | undefined | null): string {
57
- return (agentId ?? "").replace(/[^a-zA-Z0-9_.-]/g, "_");
58
- }
59
-
60
- /** Write the marker atomically (tmp → rename) so the loop never reads a
61
- * partial file mid-write. Called by `agentc pause`. */
62
- export function writeLocalPauseRequest(agentId: string | undefined | null, req: LocalPauseRequest): void {
63
- mkdirSync(requestsDir(), { recursive: true });
64
- const final = markerPath(agentId);
65
- const tmp = `${final}.${randomBytes(6).toString("hex")}.tmp`;
66
- writeFileSync(tmp, JSON.stringify(req), "utf8");
67
- renameSync(tmp, final);
68
- }
69
-
70
- /** Take-once read: returns + deletes this agent's pending pause request, else
71
- * null. Checks the agent-scoped slot first, then the unscoped fallback. The
72
- * loop calls this once per turn; a malformed marker is dropped (deleted +
73
- * null) rather than wedging the loop. */
74
- export function consumeLocalPauseRequest(agentId: string | undefined | null): LocalPauseRequest | null {
75
- const paths = [...new Set([markerPath(agentId), markerPath(null)])]; // dedupe when agentId is absent
76
- for (const path of paths) {
77
- if (!existsSync(path)) continue;
78
- try {
79
- const raw = JSON.parse(readFileSync(path, "utf8")) as unknown;
80
- rmSync(path, { force: true });
81
- if (raw && typeof raw === "object" && typeof (raw as LocalPauseRequest).reason === "string" && (raw as LocalPauseRequest).reason.trim().length > 0) {
82
- const r = raw as LocalPauseRequest;
83
- return { reason: r.reason.trim(), ...(Array.isArray(r.options) && r.options.length > 0 ? { options: r.options } : {}) };
84
- }
85
- } catch {
86
- rmSync(path, { force: true }); // unreadable / partial → drop it
87
- }
88
- }
89
- return null;
90
- }