oorja-harness-host 0.1.0 → 0.2.1
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/README.md +11 -0
- package/dist/database/index.d.ts +6 -0
- package/dist/database/index.js +29 -0
- package/dist/harnesses/doctor.js +4 -3
- package/dist/harnesses/runtime.js +1 -1
- package/dist/{utils.d.ts → host/Future.d.ts} +1 -1
- package/dist/{HarnessHost.d.ts → host/HarnessHost.d.ts} +7 -5
- package/dist/{HarnessHost.js → host/HarnessHost.js} +45 -13
- package/dist/{models.d.ts → host/models.d.ts} +1 -1
- package/dist/{models.js → host/models.js} +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +1 -2
- package/dist/messages.d.ts +1 -1
- package/dist/repo/HarnessRepo.d.ts +21 -0
- package/dist/repo/HarnessRepo.js +76 -0
- package/dist/repo/index.d.ts +2 -0
- package/dist/repo/index.js +2 -0
- package/dist/repo/migrations/001-initial.d.ts +2 -0
- package/dist/repo/migrations/001-initial.js +26 -0
- package/dist/repo/migrations/index.d.ts +2 -0
- package/dist/repo/migrations/index.js +2 -0
- package/dist/sandbox/local.d.ts +5 -2
- package/dist/sandbox/local.js +11 -5
- package/package.json +9 -8
- package/dist/resumeState/file.d.ts +0 -2
- package/dist/resumeState/file.js +0 -60
- package/dist/resumeState.d.ts +0 -8
- package/dist/resumeState.js +0 -1
- /package/dist/{utils.js → host/Future.js} +0 -0
- /package/dist/{activity.d.ts → host/activity.d.ts} +0 -0
- /package/dist/{activity.js → host/activity.js} +0 -0
package/README.md
CHANGED
|
@@ -10,3 +10,14 @@ machine identity, and command-line presentation.
|
|
|
10
10
|
Turn output remains buffered in memory until the client confirms that it has
|
|
11
11
|
saved it. On graceful shutdown, the host saves the opaque AI SDK resume state
|
|
12
12
|
provided by each harness session so the next process can resume it.
|
|
13
|
+
|
|
14
|
+
Harness state is stored in one SQLite database. The host keeps exclusive access
|
|
15
|
+
to that database while it is running; `stop()` saves the sessions and closes
|
|
16
|
+
the database.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
const host = await HarnessHost.start({
|
|
20
|
+
// ...
|
|
21
|
+
stateFile: '/path/to/harness-host.sqlite',
|
|
22
|
+
})
|
|
23
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { mkdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
export const openDatabase = (file, migrations) => {
|
|
5
|
+
mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
6
|
+
const database = new DatabaseSync(file, { timeout: 0 });
|
|
7
|
+
try {
|
|
8
|
+
database.exec('PRAGMA locking_mode = EXCLUSIVE');
|
|
9
|
+
database.exec('PRAGMA journal_mode = DELETE');
|
|
10
|
+
database.exec('BEGIN EXCLUSIVE');
|
|
11
|
+
const { user_version: currentVersion } = database
|
|
12
|
+
.prepare('PRAGMA user_version')
|
|
13
|
+
.get();
|
|
14
|
+
for (const migration of migrations) {
|
|
15
|
+
if (migration.version <= currentVersion)
|
|
16
|
+
continue;
|
|
17
|
+
migration.apply(database);
|
|
18
|
+
database.exec(`PRAGMA user_version = ${migration.version}`);
|
|
19
|
+
}
|
|
20
|
+
database.exec('COMMIT');
|
|
21
|
+
return database;
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (database.isTransaction)
|
|
25
|
+
database.exec('ROLLBACK');
|
|
26
|
+
database.close();
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
};
|
package/dist/harnesses/doctor.js
CHANGED
|
@@ -10,12 +10,13 @@ export const runHarnessDoctor = async (cwd, harnessName) => {
|
|
|
10
10
|
home ? canAccess(home, constants.R_OK | constants.W_OK) : false,
|
|
11
11
|
checkLoopbackPort().then(() => true, () => false),
|
|
12
12
|
]);
|
|
13
|
-
const nodeMajor =
|
|
13
|
+
const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
|
|
14
|
+
const nodeIsSupported = nodeMajor > 24 || (nodeMajor === 24 && nodeMinor >= 15);
|
|
14
15
|
return [
|
|
15
16
|
{
|
|
16
17
|
name: 'node',
|
|
17
|
-
ok:
|
|
18
|
-
detail: `Node ${process.versions.node}; requires Node
|
|
18
|
+
ok: nodeIsSupported,
|
|
19
|
+
detail: `Node ${process.versions.node}; requires Node 24.15 or newer`,
|
|
19
20
|
},
|
|
20
21
|
{
|
|
21
22
|
name: 'pnpm',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'node:net';
|
|
2
2
|
import { HarnessAgent } from '@ai-sdk/harness/agent';
|
|
3
3
|
import { jsonSchema } from 'ai';
|
|
4
|
-
import { HARNESS_MODELS } from '../models.js';
|
|
4
|
+
import { HARNESS_MODELS } from '../host/models.js';
|
|
5
5
|
import { LocalSandboxSession } from '../sandbox/local.js';
|
|
6
6
|
export const createHarnessRuntime = ({ harnessId, cwd, createHarness, }) => ({
|
|
7
7
|
harnessId,
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { type HarnessCommand, type HarnessEvent, type HostState } from '
|
|
2
|
-
import type {
|
|
3
|
-
import type { HarnessRuntime } from './runtime.js';
|
|
1
|
+
import { type HarnessCommand, type HarnessEvent, type HostState } from '../messages.js';
|
|
2
|
+
import type { HarnessRuntime } from '../runtime.js';
|
|
4
3
|
export type HarnessHostOptions = {
|
|
5
4
|
hostId: string;
|
|
6
5
|
hostname: string;
|
|
6
|
+
stateFile: string;
|
|
7
7
|
runtimes: HarnessRuntime[];
|
|
8
|
-
resumeStates: ResumeStateStore;
|
|
9
8
|
emit: (event: HarnessEvent) => void;
|
|
10
9
|
onError: (error: unknown) => void;
|
|
11
10
|
};
|
|
12
11
|
export declare class HarnessHost {
|
|
13
12
|
private readonly options;
|
|
13
|
+
private readonly database;
|
|
14
|
+
private readonly repo;
|
|
14
15
|
private readonly runtimes;
|
|
15
16
|
private readonly sessions;
|
|
16
17
|
private readonly openingSessions;
|
|
17
18
|
private stopping;
|
|
18
|
-
constructor(
|
|
19
|
+
private constructor();
|
|
20
|
+
static start: (options: HarnessHostOptions) => Promise<HarnessHost>;
|
|
19
21
|
getState: () => HostState;
|
|
20
22
|
handleCommand: (command: HarnessCommand) => Promise<void>;
|
|
21
23
|
stop: () => Promise<void>;
|
|
@@ -1,19 +1,42 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { smoothStream } from 'ai';
|
|
3
|
-
import { HarnessTurnPartType, MessageType, SessionStatus, TurnOutcome, } from '
|
|
3
|
+
import { HarnessTurnPartType, MessageType, SessionStatus, TurnOutcome, } from '../messages.js';
|
|
4
4
|
import { createTurnActivity, updateTurnActivity, } from './activity.js';
|
|
5
5
|
import { HARNESS_MODELS } from './models.js';
|
|
6
|
-
import {
|
|
6
|
+
import { HarnessRepo, harnessMigrations } from '../repo/index.js';
|
|
7
|
+
import { openDatabase } from '../database/index.js';
|
|
8
|
+
import { Future } from './Future.js';
|
|
7
9
|
export class HarnessHost {
|
|
8
10
|
options;
|
|
11
|
+
database;
|
|
12
|
+
repo;
|
|
9
13
|
runtimes;
|
|
10
14
|
sessions = new Map();
|
|
11
15
|
openingSessions = new Map();
|
|
12
16
|
stopping = false;
|
|
13
|
-
constructor(options) {
|
|
17
|
+
constructor(options, database, repo) {
|
|
14
18
|
this.options = options;
|
|
19
|
+
this.database = database;
|
|
20
|
+
this.repo = repo;
|
|
15
21
|
this.runtimes = new Map(options.runtimes.map((runtime) => [runtime.harnessId, runtime]));
|
|
16
22
|
}
|
|
23
|
+
static start = async (options) => {
|
|
24
|
+
const database = openDatabase(options.stateFile, harnessMigrations);
|
|
25
|
+
try {
|
|
26
|
+
const repo = new HarnessRepo(database);
|
|
27
|
+
const storedHostId = repo.getHostId();
|
|
28
|
+
if (storedHostId && storedHostId !== options.hostId) {
|
|
29
|
+
throw new Error(`Harness state belongs to host ${storedHostId}, not ${options.hostId}`);
|
|
30
|
+
}
|
|
31
|
+
if (!storedHostId)
|
|
32
|
+
repo.saveHostId(options.hostId);
|
|
33
|
+
return new HarnessHost(options, database, repo);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
database.close();
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
17
40
|
getState = () => ({
|
|
18
41
|
type: MessageType.HOST_STATE,
|
|
19
42
|
hostId: this.options.hostId,
|
|
@@ -53,12 +76,17 @@ export class HarnessHost {
|
|
|
53
76
|
};
|
|
54
77
|
stop = async () => {
|
|
55
78
|
this.stopping = true;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
79
|
+
try {
|
|
80
|
+
await Promise.allSettled(this.openingSessions.values());
|
|
81
|
+
const sessions = [...this.sessions.values()];
|
|
82
|
+
sessions.forEach(({ activeTurn }) => activeTurn?.abortController.abort());
|
|
83
|
+
await Promise.allSettled(sessions.map(({ activeTurn }) => activeTurn?.completion.promise));
|
|
84
|
+
await Promise.allSettled(sessions.map(this.saveSession));
|
|
85
|
+
this.sessions.clear();
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
this.database.close();
|
|
89
|
+
}
|
|
62
90
|
};
|
|
63
91
|
submitMessage = async (command) => {
|
|
64
92
|
const session = await this.getSession(command);
|
|
@@ -226,7 +254,7 @@ export class HarnessHost {
|
|
|
226
254
|
await session.runtime.destroy();
|
|
227
255
|
this.sessions.delete(command.sessionId);
|
|
228
256
|
}
|
|
229
|
-
|
|
257
|
+
this.repo.deleteSession(command.harnessId, command.sessionId);
|
|
230
258
|
this.emitState();
|
|
231
259
|
};
|
|
232
260
|
setSessionModel = (command) => {
|
|
@@ -262,7 +290,11 @@ export class HarnessHost {
|
|
|
262
290
|
}
|
|
263
291
|
};
|
|
264
292
|
openSession = async (runtime, sessionId) => {
|
|
265
|
-
const
|
|
293
|
+
const failure = this.repo.getResumeFailure(runtime.harnessId, sessionId);
|
|
294
|
+
if (failure) {
|
|
295
|
+
throw new Error(`Cannot resume ${runtime.harnessId} session ${sessionId}: checkpoint failed at ${failure.timestamp}: ${failure.message}`);
|
|
296
|
+
}
|
|
297
|
+
const resumeFrom = this.repo.getResumeState(runtime.harnessId, sessionId);
|
|
266
298
|
const session = {
|
|
267
299
|
harnessId: runtime.harnessId,
|
|
268
300
|
sessionId,
|
|
@@ -297,11 +329,11 @@ export class HarnessHost {
|
|
|
297
329
|
saveSession = async (session) => {
|
|
298
330
|
try {
|
|
299
331
|
const state = await session.runtime.stop();
|
|
300
|
-
|
|
332
|
+
this.repo.saveResumeState(session.harnessId, session.sessionId, state);
|
|
301
333
|
}
|
|
302
334
|
catch (error) {
|
|
303
335
|
this.options.onError(error);
|
|
304
|
-
|
|
336
|
+
this.repo.saveResumeFailure(session.harnessId, session.sessionId, errorMessage(error));
|
|
305
337
|
throw error;
|
|
306
338
|
}
|
|
307
339
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { HarnessId, type HarnessModels } from '
|
|
1
|
+
import { HarnessId, type HarnessModels } from '../messages.js';
|
|
2
2
|
export declare const HARNESS_MODELS: Record<HarnessId, HarnessModels>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
export { HarnessHost, type HarnessHostOptions } from './HarnessHost.js';
|
|
1
|
+
export { HarnessHost, type HarnessHostOptions } from './host/HarnessHost.js';
|
|
2
2
|
export { ALL_HARNESSES, createHarnessRuntimes, doctorHarnessHost, prepareHarnessHost, type HarnessSelection, } from './harnesses/index.js';
|
|
3
3
|
export { createClaudeCodeRuntime, type ClaudeCodeConfig, } from './harnesses/claudeCode/index.js';
|
|
4
4
|
export { createCodexRuntime, type CodexConfig } from './harnesses/codex/index.js';
|
|
5
5
|
export { createOpenCodeRuntime, type OpenCodeConfig } from './harnesses/openCode/index.js';
|
|
6
6
|
export * from './messages.js';
|
|
7
|
-
export { createFileResumeStateStore } from './resumeState/file.js';
|
|
8
|
-
export type { ResumeStateStore } from './resumeState.js';
|
|
9
7
|
export type { HarnessRuntime, RuntimeSession } from './runtime.js';
|
|
10
|
-
export type { ChangedFile, TurnActivity } from './activity.js';
|
|
8
|
+
export type { ChangedFile, TurnActivity } from './host/activity.js';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
export { HarnessHost } from './HarnessHost.js';
|
|
1
|
+
export { HarnessHost } from './host/HarnessHost.js';
|
|
2
2
|
export { ALL_HARNESSES, createHarnessRuntimes, doctorHarnessHost, prepareHarnessHost, } from './harnesses/index.js';
|
|
3
3
|
export { createClaudeCodeRuntime, } from './harnesses/claudeCode/index.js';
|
|
4
4
|
export { createCodexRuntime } from './harnesses/codex/index.js';
|
|
5
5
|
export { createOpenCodeRuntime } from './harnesses/openCode/index.js';
|
|
6
6
|
export * from './messages.js';
|
|
7
|
-
export { createFileResumeStateStore } from './resumeState/file.js';
|
package/dist/messages.d.ts
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import type { HarnessAgentResumeSessionState } from '@ai-sdk/harness/agent';
|
|
3
|
+
import type { HarnessId } from '../messages.js';
|
|
4
|
+
export declare class HarnessRepo {
|
|
5
|
+
private readonly database;
|
|
6
|
+
constructor(database: DatabaseSync);
|
|
7
|
+
getHostId: () => string | undefined;
|
|
8
|
+
saveHostId: (hostId: string) => void;
|
|
9
|
+
getResumeState: (harnessId: HarnessId, sessionId: string) => HarnessAgentResumeSessionState | undefined;
|
|
10
|
+
getResumeFailure: (harnessId: HarnessId, sessionId: string) => ResumeFailure | undefined;
|
|
11
|
+
saveResumeState: (harnessId: HarnessId, sessionId: string, state: HarnessAgentResumeSessionState) => void;
|
|
12
|
+
saveResumeFailure: (harnessId: HarnessId, sessionId: string, message: string) => void;
|
|
13
|
+
deleteSession: (harnessId: HarnessId, sessionId: string) => void;
|
|
14
|
+
private deleteResumeFailure;
|
|
15
|
+
private transaction;
|
|
16
|
+
}
|
|
17
|
+
type ResumeFailure = {
|
|
18
|
+
message: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
};
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export class HarnessRepo {
|
|
2
|
+
database;
|
|
3
|
+
constructor(database) {
|
|
4
|
+
this.database = database;
|
|
5
|
+
}
|
|
6
|
+
getHostId = () => {
|
|
7
|
+
const row = this.database
|
|
8
|
+
.prepare('SELECT id FROM host WHERE singleton = 1')
|
|
9
|
+
.get();
|
|
10
|
+
return row?.id;
|
|
11
|
+
};
|
|
12
|
+
saveHostId = (hostId) => {
|
|
13
|
+
this.database
|
|
14
|
+
.prepare('INSERT INTO host (singleton, id) VALUES (1, ?)')
|
|
15
|
+
.run(hostId);
|
|
16
|
+
};
|
|
17
|
+
getResumeState = (harnessId, sessionId) => {
|
|
18
|
+
const row = this.database
|
|
19
|
+
.prepare('SELECT state FROM resume_state WHERE harness_id = ? AND session_id = ?')
|
|
20
|
+
.get(harnessId, sessionId);
|
|
21
|
+
return row
|
|
22
|
+
? JSON.parse(row.state)
|
|
23
|
+
: undefined;
|
|
24
|
+
};
|
|
25
|
+
getResumeFailure = (harnessId, sessionId) => this.database
|
|
26
|
+
.prepare('SELECT message, timestamp FROM resume_failure WHERE harness_id = ? AND session_id = ?')
|
|
27
|
+
.get(harnessId, sessionId);
|
|
28
|
+
saveResumeState = (harnessId, sessionId, state) => {
|
|
29
|
+
this.transaction(() => {
|
|
30
|
+
this.database
|
|
31
|
+
.prepare(`
|
|
32
|
+
INSERT INTO resume_state (harness_id, session_id, state)
|
|
33
|
+
VALUES (?, ?, ?)
|
|
34
|
+
ON CONFLICT (harness_id, session_id)
|
|
35
|
+
DO UPDATE SET state = excluded.state
|
|
36
|
+
`)
|
|
37
|
+
.run(harnessId, sessionId, JSON.stringify(state));
|
|
38
|
+
this.deleteResumeFailure(harnessId, sessionId);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
saveResumeFailure = (harnessId, sessionId, message) => {
|
|
42
|
+
this.database
|
|
43
|
+
.prepare(`
|
|
44
|
+
INSERT INTO resume_failure (harness_id, session_id, message, timestamp)
|
|
45
|
+
VALUES (?, ?, ?, ?)
|
|
46
|
+
ON CONFLICT (harness_id, session_id)
|
|
47
|
+
DO UPDATE SET message = excluded.message, timestamp = excluded.timestamp
|
|
48
|
+
`)
|
|
49
|
+
.run(harnessId, sessionId, message, new Date().toISOString());
|
|
50
|
+
};
|
|
51
|
+
deleteSession = (harnessId, sessionId) => {
|
|
52
|
+
this.transaction(() => {
|
|
53
|
+
this.database
|
|
54
|
+
.prepare('DELETE FROM resume_state WHERE harness_id = ? AND session_id = ?')
|
|
55
|
+
.run(harnessId, sessionId);
|
|
56
|
+
this.deleteResumeFailure(harnessId, sessionId);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
deleteResumeFailure = (harnessId, sessionId) => {
|
|
60
|
+
this.database
|
|
61
|
+
.prepare('DELETE FROM resume_failure WHERE harness_id = ? AND session_id = ?')
|
|
62
|
+
.run(harnessId, sessionId);
|
|
63
|
+
};
|
|
64
|
+
transaction = (operation) => {
|
|
65
|
+
this.database.exec('BEGIN');
|
|
66
|
+
try {
|
|
67
|
+
const result = operation();
|
|
68
|
+
this.database.exec('COMMIT');
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
this.database.exec('ROLLBACK');
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const initialMigration = {
|
|
2
|
+
version: 1,
|
|
3
|
+
apply: (database) => {
|
|
4
|
+
database.exec(`
|
|
5
|
+
CREATE TABLE host (
|
|
6
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
7
|
+
id TEXT NOT NULL
|
|
8
|
+
) STRICT;
|
|
9
|
+
|
|
10
|
+
CREATE TABLE resume_state (
|
|
11
|
+
harness_id TEXT NOT NULL,
|
|
12
|
+
session_id TEXT NOT NULL,
|
|
13
|
+
state TEXT NOT NULL,
|
|
14
|
+
PRIMARY KEY (harness_id, session_id)
|
|
15
|
+
) STRICT;
|
|
16
|
+
|
|
17
|
+
CREATE TABLE resume_failure (
|
|
18
|
+
harness_id TEXT NOT NULL,
|
|
19
|
+
session_id TEXT NOT NULL,
|
|
20
|
+
message TEXT NOT NULL,
|
|
21
|
+
timestamp TEXT NOT NULL,
|
|
22
|
+
PRIMARY KEY (harness_id, session_id)
|
|
23
|
+
) STRICT;
|
|
24
|
+
`);
|
|
25
|
+
},
|
|
26
|
+
};
|
package/dist/sandbox/local.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { Experimental_SandboxSession } from 'ai';
|
|
1
|
+
import type { Experimental_SandboxProcess, Experimental_SandboxSession } from 'ai';
|
|
2
|
+
import type { Experimental_SandboxSpawnOptions as SandboxSpawnOptions, Experimental_SandboxSpawnOptionsWithStdin as SandboxSpawnOptionsWithStdin, Experimental_SandboxSpawnOptionsWithoutStdin as SandboxSpawnOptionsWithoutStdin, Experimental_WritableSandboxProcess as WritableSandboxProcess } from '@ai-sdk/provider-utils';
|
|
2
3
|
export declare class LocalSandboxSession implements Experimental_SandboxSession {
|
|
3
4
|
private readonly cwd;
|
|
4
5
|
readonly description: string;
|
|
@@ -10,6 +11,8 @@ export declare class LocalSandboxSession implements Experimental_SandboxSession
|
|
|
10
11
|
writeBinaryFile: Experimental_SandboxSession['writeBinaryFile'];
|
|
11
12
|
writeFile: Experimental_SandboxSession['writeFile'];
|
|
12
13
|
writeTextFile: Experimental_SandboxSession['writeTextFile'];
|
|
13
|
-
spawn:
|
|
14
|
+
spawn(options: SandboxSpawnOptionsWithStdin): Promise<WritableSandboxProcess>;
|
|
15
|
+
spawn(options: SandboxSpawnOptionsWithoutStdin): Promise<Experimental_SandboxProcess>;
|
|
16
|
+
spawn(options: SandboxSpawnOptions): Promise<Experimental_SandboxProcess | WritableSandboxProcess>;
|
|
14
17
|
run: Experimental_SandboxSession['run'];
|
|
15
18
|
}
|
package/dist/sandbox/local.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn as spawnChild } from 'node:child_process';
|
|
2
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { Readable } from 'node:stream';
|
|
4
|
+
import { Readable, Writable } from 'node:stream';
|
|
5
5
|
export class LocalSandboxSession {
|
|
6
6
|
cwd;
|
|
7
7
|
description;
|
|
@@ -63,7 +63,7 @@ export class LocalSandboxSession {
|
|
|
63
63
|
abortSignal,
|
|
64
64
|
});
|
|
65
65
|
};
|
|
66
|
-
|
|
66
|
+
async spawn({ command, workingDirectory, env, abortSignal, stdin = false, }) {
|
|
67
67
|
if (abortSignal?.aborted)
|
|
68
68
|
throw abortSignal.reason;
|
|
69
69
|
const child = spawnChild(command, {
|
|
@@ -72,7 +72,7 @@ export class LocalSandboxSession {
|
|
|
72
72
|
shell: true,
|
|
73
73
|
// Terminal Ctrl-C must reach the host first so it can save the harness session.
|
|
74
74
|
detached: process.platform !== 'win32',
|
|
75
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
stdio: [stdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
|
76
76
|
});
|
|
77
77
|
const stdout = Readable.toWeb(child.stdout);
|
|
78
78
|
const stderr = Readable.toWeb(child.stderr);
|
|
@@ -111,8 +111,14 @@ export class LocalSandboxSession {
|
|
|
111
111
|
};
|
|
112
112
|
abortSignal?.addEventListener('abort', handleAbort, { once: true });
|
|
113
113
|
void wait.finally(() => abortSignal?.removeEventListener('abort', handleAbort)).catch(() => { });
|
|
114
|
-
|
|
115
|
-
|
|
114
|
+
const sandboxProcess = { pid: child.pid, stdout, stderr, wait: () => wait, kill };
|
|
115
|
+
if (!stdin)
|
|
116
|
+
return sandboxProcess;
|
|
117
|
+
return {
|
|
118
|
+
...sandboxProcess,
|
|
119
|
+
stdin: Writable.toWeb(child.stdin),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
116
122
|
run = async (options) => {
|
|
117
123
|
const process = await this.spawn(options);
|
|
118
124
|
const [stdout, stderr, result] = await Promise.all([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oorja-harness-host",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Host AI coding harness sessions on a Supakit-connected computer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -14,16 +14,17 @@
|
|
|
14
14
|
"dist"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "tsc -b && tsc-alias -p tsconfig.json --resolve-full-paths --resolve-full-extension .js",
|
|
17
|
+
"build": "pnpm run clean && tsc -b --force && tsc-alias -p tsconfig.json --resolve-full-paths --resolve-full-extension .js",
|
|
18
18
|
"clean": "shx rm -rf dist",
|
|
19
19
|
"prepack": "pnpm run build"
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@ai-sdk/harness": "1.0.
|
|
23
|
-
"@ai-sdk/harness-claude-code": "1.0.
|
|
24
|
-
"@ai-sdk/harness-codex": "1.0.
|
|
25
|
-
"@ai-sdk/harness-opencode": "1.0.
|
|
26
|
-
"ai": "
|
|
22
|
+
"@ai-sdk/harness": "1.0.167",
|
|
23
|
+
"@ai-sdk/harness-claude-code": "1.0.171",
|
|
24
|
+
"@ai-sdk/harness-codex": "1.0.169",
|
|
25
|
+
"@ai-sdk/harness-opencode": "1.0.169",
|
|
26
|
+
"@ai-sdk/provider-utils": "5.0.70",
|
|
27
|
+
"ai": "7.0.156"
|
|
27
28
|
},
|
|
28
29
|
"devDependencies": {
|
|
29
30
|
"@types/node": "^22.10.2",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"typescript": "^5.9.2"
|
|
33
34
|
},
|
|
34
35
|
"engines": {
|
|
35
|
-
"node": ">=
|
|
36
|
+
"node": ">=24.15.0"
|
|
36
37
|
},
|
|
37
38
|
"packageManager": "pnpm@11.5.2"
|
|
38
39
|
}
|
package/dist/resumeState/file.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
export const createFileResumeStateStore = (directory) => ({
|
|
4
|
-
load: async (harnessId, sessionId) => {
|
|
5
|
-
try {
|
|
6
|
-
const failure = JSON.parse(await readFile(failurePath(directory, harnessId, sessionId), 'utf8'));
|
|
7
|
-
throw new Error(`Cannot resume ${harnessId} session ${sessionId}: checkpoint failed at ${failure.timestamp}: ${failure.message}`);
|
|
8
|
-
}
|
|
9
|
-
catch (error) {
|
|
10
|
-
if (!isMissingFile(error))
|
|
11
|
-
throw error;
|
|
12
|
-
}
|
|
13
|
-
try {
|
|
14
|
-
const stored = JSON.parse(await readFile(statePath(directory, harnessId, sessionId), 'utf8'));
|
|
15
|
-
return stored.state;
|
|
16
|
-
}
|
|
17
|
-
catch (error) {
|
|
18
|
-
if (isMissingFile(error))
|
|
19
|
-
return;
|
|
20
|
-
throw error;
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
save: async (harnessId, sessionId, state) => {
|
|
24
|
-
const destination = statePath(directory, harnessId, sessionId);
|
|
25
|
-
await saveRecord(destination, { harnessId, sessionId, state });
|
|
26
|
-
await removeFile(failurePath(directory, harnessId, sessionId));
|
|
27
|
-
},
|
|
28
|
-
markFailed: async (harnessId, sessionId, message) => {
|
|
29
|
-
const failure = {
|
|
30
|
-
harnessId,
|
|
31
|
-
sessionId,
|
|
32
|
-
timestamp: new Date().toISOString(),
|
|
33
|
-
message,
|
|
34
|
-
};
|
|
35
|
-
await saveRecord(failurePath(directory, harnessId, sessionId), failure);
|
|
36
|
-
},
|
|
37
|
-
remove: async (harnessId, sessionId) => {
|
|
38
|
-
await removeFile(statePath(directory, harnessId, sessionId));
|
|
39
|
-
await removeFile(failurePath(directory, harnessId, sessionId));
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
const statePath = (directory, harnessId, sessionId) => path.join(directory, `${encode(harnessId)}-${encode(sessionId)}.json`);
|
|
43
|
-
const failurePath = (directory, harnessId, sessionId) => path.join(directory, `${encode(harnessId)}-${encode(sessionId)}.failure.json`);
|
|
44
|
-
const saveRecord = async (destination, record) => {
|
|
45
|
-
await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
|
|
46
|
-
const temporary = `${destination}.${process.pid}.tmp`;
|
|
47
|
-
await writeFile(temporary, JSON.stringify(record), { encoding: 'utf8', mode: 0o600 });
|
|
48
|
-
await rename(temporary, destination);
|
|
49
|
-
};
|
|
50
|
-
const removeFile = async (file) => {
|
|
51
|
-
try {
|
|
52
|
-
await unlink(file);
|
|
53
|
-
}
|
|
54
|
-
catch (error) {
|
|
55
|
-
if (!isMissingFile(error))
|
|
56
|
-
throw error;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
const encode = (value) => Buffer.from(value).toString('base64url');
|
|
60
|
-
const isMissingFile = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
package/dist/resumeState.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { HarnessAgentResumeSessionState } from '@ai-sdk/harness/agent';
|
|
2
|
-
import type { HarnessId } from './messages.js';
|
|
3
|
-
export type ResumeStateStore = {
|
|
4
|
-
load: (harnessId: HarnessId, sessionId: string) => Promise<HarnessAgentResumeSessionState | undefined>;
|
|
5
|
-
save: (harnessId: HarnessId, sessionId: string, state: HarnessAgentResumeSessionState) => Promise<void>;
|
|
6
|
-
markFailed: (harnessId: HarnessId, sessionId: string, message: string) => Promise<void>;
|
|
7
|
-
remove: (harnessId: HarnessId, sessionId: string) => Promise<void>;
|
|
8
|
-
};
|
package/dist/resumeState.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|