@agentstep/agent-sdk 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 (105) hide show
  1. package/package.json +45 -0
  2. package/src/auth/middleware.ts +38 -0
  3. package/src/backends/claude/args.ts +88 -0
  4. package/src/backends/claude/index.ts +193 -0
  5. package/src/backends/claude/permission-hook.ts +152 -0
  6. package/src/backends/claude/tool-bridge.ts +211 -0
  7. package/src/backends/claude/translator.ts +209 -0
  8. package/src/backends/claude/wrapper-script.ts +45 -0
  9. package/src/backends/codex/args.ts +69 -0
  10. package/src/backends/codex/auth.ts +35 -0
  11. package/src/backends/codex/index.ts +57 -0
  12. package/src/backends/codex/setup.ts +37 -0
  13. package/src/backends/codex/translator.ts +223 -0
  14. package/src/backends/codex/wrapper-script.ts +26 -0
  15. package/src/backends/factory/args.ts +45 -0
  16. package/src/backends/factory/auth.ts +30 -0
  17. package/src/backends/factory/index.ts +56 -0
  18. package/src/backends/factory/setup.ts +34 -0
  19. package/src/backends/factory/translator.ts +139 -0
  20. package/src/backends/factory/wrapper-script.ts +33 -0
  21. package/src/backends/gemini/args.ts +44 -0
  22. package/src/backends/gemini/auth.ts +30 -0
  23. package/src/backends/gemini/index.ts +53 -0
  24. package/src/backends/gemini/setup.ts +34 -0
  25. package/src/backends/gemini/translator.ts +139 -0
  26. package/src/backends/gemini/wrapper-script.ts +26 -0
  27. package/src/backends/opencode/args.ts +53 -0
  28. package/src/backends/opencode/auth.ts +53 -0
  29. package/src/backends/opencode/index.ts +70 -0
  30. package/src/backends/opencode/mcp.ts +67 -0
  31. package/src/backends/opencode/setup.ts +54 -0
  32. package/src/backends/opencode/translator.ts +168 -0
  33. package/src/backends/opencode/wrapper-script.ts +46 -0
  34. package/src/backends/registry.ts +38 -0
  35. package/src/backends/shared/ndjson.ts +29 -0
  36. package/src/backends/shared/translator-types.ts +69 -0
  37. package/src/backends/shared/wrap-prompt.ts +17 -0
  38. package/src/backends/types.ts +85 -0
  39. package/src/config/index.ts +95 -0
  40. package/src/db/agents.ts +185 -0
  41. package/src/db/api_keys.ts +78 -0
  42. package/src/db/batch.ts +142 -0
  43. package/src/db/client.ts +81 -0
  44. package/src/db/environments.ts +127 -0
  45. package/src/db/events.ts +208 -0
  46. package/src/db/memory.ts +143 -0
  47. package/src/db/migrations.ts +295 -0
  48. package/src/db/proxy.ts +37 -0
  49. package/src/db/sessions.ts +295 -0
  50. package/src/db/vaults.ts +110 -0
  51. package/src/errors.ts +53 -0
  52. package/src/handlers/agents.ts +194 -0
  53. package/src/handlers/batch.ts +41 -0
  54. package/src/handlers/docs.ts +87 -0
  55. package/src/handlers/environments.ts +154 -0
  56. package/src/handlers/events.ts +234 -0
  57. package/src/handlers/index.ts +12 -0
  58. package/src/handlers/memory.ts +141 -0
  59. package/src/handlers/openapi.ts +14 -0
  60. package/src/handlers/sessions.ts +223 -0
  61. package/src/handlers/stream.ts +76 -0
  62. package/src/handlers/threads.ts +26 -0
  63. package/src/handlers/ui/app.js +984 -0
  64. package/src/handlers/ui/index.html +112 -0
  65. package/src/handlers/ui/style.css +164 -0
  66. package/src/handlers/ui.ts +1281 -0
  67. package/src/handlers/vaults.ts +99 -0
  68. package/src/http.ts +35 -0
  69. package/src/index.ts +104 -0
  70. package/src/init.ts +227 -0
  71. package/src/openapi/registry.ts +8 -0
  72. package/src/openapi/schemas.ts +625 -0
  73. package/src/openapi/spec.ts +691 -0
  74. package/src/providers/apple.ts +220 -0
  75. package/src/providers/daytona.ts +217 -0
  76. package/src/providers/docker.ts +264 -0
  77. package/src/providers/e2b.ts +203 -0
  78. package/src/providers/fly.ts +276 -0
  79. package/src/providers/modal.ts +222 -0
  80. package/src/providers/podman.ts +206 -0
  81. package/src/providers/registry.ts +28 -0
  82. package/src/providers/shared.ts +11 -0
  83. package/src/providers/sprites.ts +55 -0
  84. package/src/providers/types.ts +73 -0
  85. package/src/providers/vercel.ts +208 -0
  86. package/src/proxy/forward.ts +111 -0
  87. package/src/queue/index.ts +111 -0
  88. package/src/sessions/actor.ts +53 -0
  89. package/src/sessions/bus.ts +155 -0
  90. package/src/sessions/driver.ts +818 -0
  91. package/src/sessions/grader.ts +120 -0
  92. package/src/sessions/interrupt.ts +14 -0
  93. package/src/sessions/sweeper.ts +136 -0
  94. package/src/sessions/threads.ts +126 -0
  95. package/src/sessions/tools.ts +50 -0
  96. package/src/shutdown.ts +78 -0
  97. package/src/sprite/client.ts +294 -0
  98. package/src/sprite/exec.ts +161 -0
  99. package/src/sprite/lifecycle.ts +339 -0
  100. package/src/sprite/pool.ts +65 -0
  101. package/src/sprite/setup.ts +159 -0
  102. package/src/state.ts +61 -0
  103. package/src/types.ts +339 -0
  104. package/src/util/clock.ts +7 -0
  105. package/src/util/ids.ts +11 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Turn-level concurrency queue.
3
+ *
4
+ * Enforces two limits:
5
+ * - Global: `config.concurrency` (how many turns may run simultaneously
6
+ * across all sessions/environments)
7
+ * - Per-environment: `config.maxSpritesPerEnv` (real resource constraint,
8
+ * since each running turn owns a sprite in its env's pool)
9
+ *
10
+ * Sessions are pinned to sprites 1:1, so a session's active turn naturally
11
+ * consumes one env slot. The queue tracks pending enqueues and releases
12
+ * capacity as turns complete.
13
+ *
14
+ * Pattern inspired by
15
+ *
16
+ * reparameterized per plan §Important I3.
17
+ */
18
+ import { getConfig } from "../config";
19
+ import { serverBusy } from "../errors";
20
+
21
+ interface Job<T> {
22
+ envId: string;
23
+ run: () => Promise<T>;
24
+ resolve: (v: T) => void;
25
+ reject: (e: unknown) => void;
26
+ }
27
+
28
+ type State = {
29
+ queue: Job<unknown>[];
30
+ activeGlobal: number;
31
+ activeByEnv: Map<string, number>;
32
+ };
33
+
34
+ type GlobalQueue = typeof globalThis & { __caQueue?: State };
35
+
36
+ function state(): State {
37
+ const g = globalThis as GlobalQueue;
38
+ if (!g.__caQueue) {
39
+ g.__caQueue = {
40
+ queue: [],
41
+ activeGlobal: 0,
42
+ activeByEnv: new Map(),
43
+ };
44
+ }
45
+ return g.__caQueue;
46
+ }
47
+
48
+ /**
49
+ * Enqueue a turn-runner against the given environment. Throws `serverBusy`
50
+ * immediately if the queue depth limit is exceeded.
51
+ */
52
+ export function enqueueTurn<T>(envId: string, run: () => Promise<T>): Promise<T> {
53
+ const s = state();
54
+ const cfg = getConfig();
55
+ const maxDepth = 100; // generous — each job is lightweight
56
+ if (s.queue.length >= maxDepth) {
57
+ throw serverBusy("turn queue is full");
58
+ }
59
+
60
+ return new Promise<T>((resolve, reject) => {
61
+ s.queue.push({
62
+ envId,
63
+ run: run as () => Promise<unknown>,
64
+ resolve: resolve as (v: unknown) => void,
65
+ reject,
66
+ });
67
+ drain();
68
+ });
69
+
70
+ function drain(): void {
71
+ const st = state();
72
+ const c = getConfig();
73
+ for (let i = 0; i < st.queue.length; i++) {
74
+ if (st.activeGlobal >= c.concurrency) break;
75
+ const job = st.queue[i];
76
+ const envActive = st.activeByEnv.get(job.envId) ?? 0;
77
+ if (envActive >= c.maxSpritesPerEnv) continue;
78
+
79
+ // Take this job
80
+ st.queue.splice(i, 1);
81
+ i--;
82
+ st.activeGlobal++;
83
+ st.activeByEnv.set(job.envId, envActive + 1);
84
+
85
+ void (async () => {
86
+ try {
87
+ const r = await job.run();
88
+ job.resolve(r);
89
+ } catch (err) {
90
+ job.reject(err);
91
+ } finally {
92
+ const st2 = state();
93
+ st2.activeGlobal--;
94
+ const envAct = st2.activeByEnv.get(job.envId) ?? 1;
95
+ if (envAct <= 1) st2.activeByEnv.delete(job.envId);
96
+ else st2.activeByEnv.set(job.envId, envAct - 1);
97
+ drain();
98
+ }
99
+ })();
100
+ }
101
+ }
102
+ }
103
+
104
+ export function queueStats() {
105
+ const s = state();
106
+ return {
107
+ queued: s.queue.length,
108
+ activeGlobal: s.activeGlobal,
109
+ activeByEnv: Array.from(s.activeByEnv.entries()),
110
+ };
111
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Per-session async-serialized actor.
3
+ *
4
+ * This is the concurrency primitive everything else depends on for ordering
5
+ * correctness. One actor per session; all mutating work (append events, run
6
+ * turns, interrupt, archive, delete) goes through `enqueue(fn)` which runs
7
+ * tasks serially in FIFO order.
8
+ *
9
+ * Rationale (plan §Critical fix C2): per-event SQL transactions do not
10
+ * serialize the DECISION to abort with respect to the event stream. A real
11
+ * in-memory lock is required, and the cleanest shape is a FIFO promise chain
12
+ * scoped to the session.
13
+ */
14
+ type GlobalActors = typeof globalThis & {
15
+ __caActors?: Map<string, SessionActor>;
16
+ };
17
+
18
+ export class SessionActor {
19
+ private tail: Promise<unknown> = Promise.resolve();
20
+
21
+ constructor(public readonly sessionId: string) {}
22
+
23
+ enqueue<T>(fn: () => Promise<T>): Promise<T> {
24
+ const run = this.tail.then(fn, fn);
25
+ // Swallow errors on the chain so one failed job doesn't kill the actor.
26
+ this.tail = run.catch(() => {});
27
+ return run;
28
+ }
29
+ }
30
+
31
+ function registry(): Map<string, SessionActor> {
32
+ const g = globalThis as GlobalActors;
33
+ if (!g.__caActors) g.__caActors = new Map();
34
+ return g.__caActors;
35
+ }
36
+
37
+ export function getActor(sessionId: string): SessionActor {
38
+ const reg = registry();
39
+ let actor = reg.get(sessionId);
40
+ if (!actor) {
41
+ actor = new SessionActor(sessionId);
42
+ reg.set(sessionId, actor);
43
+ }
44
+ return actor;
45
+ }
46
+
47
+ export function dropActor(sessionId: string): void {
48
+ registry().delete(sessionId);
49
+ }
50
+
51
+ export function allActorIds(): string[] {
52
+ return Array.from(registry().keys());
53
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Session event bus: append-only log + EventEmitter fan-out.
3
+ *
4
+ * The DB is authoritative; the emitter is the live tail. Callers mutate the
5
+ * session log exclusively through `appendEvent` / `appendEventsBatch`, which
6
+ * insert under an IMMEDIATE transaction and THEN emit post-commit. This is
7
+ * how the Managed Agents stream-first reconnect contract works: subscribers
8
+ * can attach anytime, backfill from the DB, and keep tailing the emitter.
9
+ *
10
+ * Every append MUST be invoked from inside the corresponding session's
11
+ * `SessionActor` (see `lib/sessions/actor.ts`) to preserve ordering.
12
+ */
13
+ import { EventEmitter } from "node:events";
14
+ import {
15
+ appendEvent as dbAppend,
16
+ appendEventsBatch as dbAppendBatch,
17
+ listEvents,
18
+ rowToManagedEvent,
19
+ type AppendInput,
20
+ } from "../db/events";
21
+ import { getSession } from "../db/sessions";
22
+ import { getAgent } from "../db/agents";
23
+ import type { EventRow, ManagedEvent } from "../types";
24
+
25
+ type GlobalBus = typeof globalThis & {
26
+ __caBusEmitters?: Map<string, EventEmitter>;
27
+ };
28
+
29
+ function emitters(): Map<string, EventEmitter> {
30
+ const g = globalThis as GlobalBus;
31
+ if (!g.__caBusEmitters) g.__caBusEmitters = new Map();
32
+ return g.__caBusEmitters;
33
+ }
34
+
35
+ /** Cached webhook config per session — avoids 2 DB reads (getSession + getAgent) per event. */
36
+ interface WebhookCacheEntry {
37
+ webhookUrl: string | null;
38
+ webhookEvents: string[];
39
+ }
40
+ const webhookCache = new Map<string, WebhookCacheEntry>();
41
+
42
+ function getWebhookConfig(sessionId: string): WebhookCacheEntry {
43
+ const cached = webhookCache.get(sessionId);
44
+ if (cached) return cached;
45
+
46
+ const session = getSession(sessionId);
47
+ if (!session) {
48
+ const entry: WebhookCacheEntry = { webhookUrl: null, webhookEvents: [] };
49
+ webhookCache.set(sessionId, entry);
50
+ return entry;
51
+ }
52
+ const agent = getAgent(session.agent.id, session.agent.version);
53
+ const entry: WebhookCacheEntry = {
54
+ webhookUrl: agent?.webhook_url ?? null,
55
+ webhookEvents: agent?.webhook_events ?? [],
56
+ };
57
+ webhookCache.set(sessionId, entry);
58
+ return entry;
59
+ }
60
+
61
+ function getOrCreateEmitter(sessionId: string): EventEmitter {
62
+ const reg = emitters();
63
+ let em = reg.get(sessionId);
64
+ if (!em) {
65
+ em = new EventEmitter();
66
+ em.setMaxListeners(0); // unbounded subscribers per session
67
+ reg.set(sessionId, em);
68
+ }
69
+ return em;
70
+ }
71
+
72
+ function fireWebhook(sessionId: string, row: EventRow): void {
73
+ try {
74
+ const config = getWebhookConfig(sessionId);
75
+ if (!config.webhookUrl) return;
76
+ if (!config.webhookEvents.includes(row.type)) return;
77
+
78
+ const payload = JSON.stringify(rowToManagedEvent(row));
79
+ void fetch(config.webhookUrl, {
80
+ method: "POST",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: payload,
83
+ signal: AbortSignal.timeout(5000),
84
+ }).catch((err: unknown) => {
85
+ console.warn(`[webhook] POST to ${config.webhookUrl} failed:`, err);
86
+ });
87
+ } catch {
88
+ // best-effort — never let webhook errors propagate
89
+ }
90
+ }
91
+
92
+ export function appendEvent(sessionId: string, input: AppendInput): EventRow {
93
+ const row = dbAppend(sessionId, input);
94
+ getOrCreateEmitter(sessionId).emit("event", row);
95
+ fireWebhook(sessionId, row);
96
+ return row;
97
+ }
98
+
99
+ export function appendEventsBatch(sessionId: string, inputs: AppendInput[]): EventRow[] {
100
+ const rows = dbAppendBatch(sessionId, inputs);
101
+ const em = getOrCreateEmitter(sessionId);
102
+ for (const row of rows) {
103
+ em.emit("event", row);
104
+ fireWebhook(sessionId, row);
105
+ }
106
+ return rows;
107
+ }
108
+
109
+ export interface Subscription {
110
+ unsubscribe(): void;
111
+ }
112
+
113
+ /**
114
+ * Subscribe to live events for a session. First emits any backlog rows with
115
+ * `seq > fromSeq` (read from the DB), then attaches a live listener. The
116
+ * backlog read and live attach happen in sequence; a small race is possible
117
+ * where an event lands between the DB read and the listener attach, so the
118
+ * live handler dedupes by tracking `lastDeliveredSeq`.
119
+ */
120
+ export function subscribe(
121
+ sessionId: string,
122
+ fromSeq: number,
123
+ onEvent: (evt: ManagedEvent) => void,
124
+ ): Subscription {
125
+ const em = getOrCreateEmitter(sessionId);
126
+ let lastDeliveredSeq = fromSeq;
127
+
128
+ // Backlog drain
129
+ const backlog = listEvents(sessionId, { limit: 500, order: "asc", afterSeq: fromSeq });
130
+ for (const row of backlog) {
131
+ onEvent(rowToManagedEvent(row));
132
+ if (row.seq > lastDeliveredSeq) lastDeliveredSeq = row.seq;
133
+ }
134
+
135
+ const handler = (row: EventRow) => {
136
+ if (row.seq <= lastDeliveredSeq) return;
137
+ onEvent(rowToManagedEvent(row));
138
+ lastDeliveredSeq = row.seq;
139
+ };
140
+ em.on("event", handler);
141
+
142
+ return {
143
+ unsubscribe() {
144
+ em.off("event", handler);
145
+ },
146
+ };
147
+ }
148
+
149
+ export function dropEmitter(sessionId: string): void {
150
+ const reg = emitters();
151
+ const em = reg.get(sessionId);
152
+ if (em) em.removeAllListeners();
153
+ reg.delete(sessionId);
154
+ webhookCache.delete(sessionId);
155
+ }