@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.
- package/package.json +45 -0
- package/src/auth/middleware.ts +38 -0
- package/src/backends/claude/args.ts +88 -0
- package/src/backends/claude/index.ts +193 -0
- package/src/backends/claude/permission-hook.ts +152 -0
- package/src/backends/claude/tool-bridge.ts +211 -0
- package/src/backends/claude/translator.ts +209 -0
- package/src/backends/claude/wrapper-script.ts +45 -0
- package/src/backends/codex/args.ts +69 -0
- package/src/backends/codex/auth.ts +35 -0
- package/src/backends/codex/index.ts +57 -0
- package/src/backends/codex/setup.ts +37 -0
- package/src/backends/codex/translator.ts +223 -0
- package/src/backends/codex/wrapper-script.ts +26 -0
- package/src/backends/factory/args.ts +45 -0
- package/src/backends/factory/auth.ts +30 -0
- package/src/backends/factory/index.ts +56 -0
- package/src/backends/factory/setup.ts +34 -0
- package/src/backends/factory/translator.ts +139 -0
- package/src/backends/factory/wrapper-script.ts +33 -0
- package/src/backends/gemini/args.ts +44 -0
- package/src/backends/gemini/auth.ts +30 -0
- package/src/backends/gemini/index.ts +53 -0
- package/src/backends/gemini/setup.ts +34 -0
- package/src/backends/gemini/translator.ts +139 -0
- package/src/backends/gemini/wrapper-script.ts +26 -0
- package/src/backends/opencode/args.ts +53 -0
- package/src/backends/opencode/auth.ts +53 -0
- package/src/backends/opencode/index.ts +70 -0
- package/src/backends/opencode/mcp.ts +67 -0
- package/src/backends/opencode/setup.ts +54 -0
- package/src/backends/opencode/translator.ts +168 -0
- package/src/backends/opencode/wrapper-script.ts +46 -0
- package/src/backends/registry.ts +38 -0
- package/src/backends/shared/ndjson.ts +29 -0
- package/src/backends/shared/translator-types.ts +69 -0
- package/src/backends/shared/wrap-prompt.ts +17 -0
- package/src/backends/types.ts +85 -0
- package/src/config/index.ts +95 -0
- package/src/db/agents.ts +185 -0
- package/src/db/api_keys.ts +78 -0
- package/src/db/batch.ts +142 -0
- package/src/db/client.ts +81 -0
- package/src/db/environments.ts +127 -0
- package/src/db/events.ts +208 -0
- package/src/db/memory.ts +143 -0
- package/src/db/migrations.ts +295 -0
- package/src/db/proxy.ts +37 -0
- package/src/db/sessions.ts +295 -0
- package/src/db/vaults.ts +110 -0
- package/src/errors.ts +53 -0
- package/src/handlers/agents.ts +194 -0
- package/src/handlers/batch.ts +41 -0
- package/src/handlers/docs.ts +87 -0
- package/src/handlers/environments.ts +154 -0
- package/src/handlers/events.ts +234 -0
- package/src/handlers/index.ts +12 -0
- package/src/handlers/memory.ts +141 -0
- package/src/handlers/openapi.ts +14 -0
- package/src/handlers/sessions.ts +223 -0
- package/src/handlers/stream.ts +76 -0
- package/src/handlers/threads.ts +26 -0
- package/src/handlers/ui/app.js +984 -0
- package/src/handlers/ui/index.html +112 -0
- package/src/handlers/ui/style.css +164 -0
- package/src/handlers/ui.ts +1281 -0
- package/src/handlers/vaults.ts +99 -0
- package/src/http.ts +35 -0
- package/src/index.ts +104 -0
- package/src/init.ts +227 -0
- package/src/openapi/registry.ts +8 -0
- package/src/openapi/schemas.ts +625 -0
- package/src/openapi/spec.ts +691 -0
- package/src/providers/apple.ts +220 -0
- package/src/providers/daytona.ts +217 -0
- package/src/providers/docker.ts +264 -0
- package/src/providers/e2b.ts +203 -0
- package/src/providers/fly.ts +276 -0
- package/src/providers/modal.ts +222 -0
- package/src/providers/podman.ts +206 -0
- package/src/providers/registry.ts +28 -0
- package/src/providers/shared.ts +11 -0
- package/src/providers/sprites.ts +55 -0
- package/src/providers/types.ts +73 -0
- package/src/providers/vercel.ts +208 -0
- package/src/proxy/forward.ts +111 -0
- package/src/queue/index.ts +111 -0
- package/src/sessions/actor.ts +53 -0
- package/src/sessions/bus.ts +155 -0
- package/src/sessions/driver.ts +818 -0
- package/src/sessions/grader.ts +120 -0
- package/src/sessions/interrupt.ts +14 -0
- package/src/sessions/sweeper.ts +136 -0
- package/src/sessions/threads.ts +126 -0
- package/src/sessions/tools.ts +50 -0
- package/src/shutdown.ts +78 -0
- package/src/sprite/client.ts +294 -0
- package/src/sprite/exec.ts +161 -0
- package/src/sprite/lifecycle.ts +339 -0
- package/src/sprite/pool.ts +65 -0
- package/src/sprite/setup.ts +159 -0
- package/src/state.ts +61 -0
- package/src/types.ts +339 -0
- package/src/util/clock.ts +7 -0
- 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
|
+
}
|