@noopolis/daimon 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Noopolis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Daimon
2
+
3
+ Daimon is the Noopolis-native per-agent runtime harness.
4
+
5
+ It defines a small per-agent contract and currently implements that contract on
6
+ top of Pi. A Daimon runs one harnessed agent inside a caller-prepared workspace.
7
+
8
+ Spawnfile should own orgs, nested teams, schedules, Moltnet wiring, workspace
9
+ resource compilation, and the app that starts many harnessed agents. This package
10
+ should not know what an org is.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install @noopolis/daimon
16
+ ```
17
+
18
+ ## Tests
19
+
20
+ The package has a non-live test suite for auth seeding and Pi model config
21
+ generation:
22
+
23
+ ```bash
24
+ npm test
25
+ npm run typecheck
26
+ npm run build
27
+ ```
28
+
29
+ These tests do not call a model provider.
30
+
31
+ ## Model And Auth Helpers
32
+
33
+ The Pi adapter supports the same model intent shape Spawnfile lowers for Pi:
34
+
35
+ - OpenAI Codex subscription auth maps to Pi's `openai-codex` OAuth auth store.
36
+ - Claude Code subscription auth maps to Pi's Anthropic auth store.
37
+ - API-key credentials can be written directly into Pi auth storage.
38
+ - Local and custom OpenAI-compatible endpoints render Pi `models.json`.
39
+
40
+ For Ollama-style local models, use a local endpoint with `auth.method: none`.
41
+ Pi still requires an API-key field for custom providers, so the helper writes the
42
+ upstream-documented dummy `ollama` value.
43
+
44
+ ## Pi E2E
45
+
46
+ The Pi E2E uses the local Codex CLI subscription auth file to seed an ignored Pi
47
+ `auth.json` under `.runtime/`.
48
+
49
+ ```bash
50
+ npm install
51
+ npm run e2e:pi-agent
52
+ ```
53
+
54
+ The example starts two harnessed Pi agents from plain caller code. The example
55
+ creates the workspaces and shared resource itself to demonstrate the intended
56
+ boundary: the caller prepares files, the harness runs agent turns.
@@ -0,0 +1,49 @@
1
+ export type AgentId = string;
2
+ export interface AgentStartInput {
3
+ id: AgentId;
4
+ name: string;
5
+ instructions: string;
6
+ workspacePath: string;
7
+ runtimeHomePath: string;
8
+ tools?: string[];
9
+ }
10
+ export interface HarnessModelEndpoint {
11
+ baseUrl: string;
12
+ compatibility: "anthropic" | "openai";
13
+ }
14
+ export interface HarnessModelSpec {
15
+ auth?: {
16
+ keyEnv?: string;
17
+ method: "api_key" | "claude-code" | "codex" | "none";
18
+ };
19
+ endpoint?: HarnessModelEndpoint;
20
+ name: string;
21
+ provider: string;
22
+ }
23
+ export interface WakeEvent {
24
+ id: string;
25
+ kind: "manual" | "message" | "schedule";
26
+ from?: string;
27
+ text: string;
28
+ }
29
+ export interface WakeResult {
30
+ agentId: AgentId;
31
+ text: string;
32
+ durationMs: number;
33
+ }
34
+ export interface AgentStatus {
35
+ agentId: AgentId;
36
+ state: "starting" | "idle" | "running" | "stopped" | "failed";
37
+ lastWakeAt?: string;
38
+ lastError?: string;
39
+ }
40
+ export interface AgentHandle {
41
+ id: AgentId;
42
+ wake(event: WakeEvent): Promise<WakeResult>;
43
+ status(): AgentStatus;
44
+ stop(): Promise<void>;
45
+ }
46
+ export interface AgentHarnessAdapter {
47
+ startAgent(input: AgentStartInput): Promise<AgentHandle>;
48
+ }
49
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAE7B,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,WAAW,GAAG,QAAQ,CAAC;CACvC;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,SAAS,GAAG,aAAa,GAAG,OAAO,GAAG,MAAM,CAAC;KACtD,CAAC;IACF,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,QAAQ,GAAG,SAAS,GAAG,UAAU,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC9D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,OAAO,CAAC;IACZ,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;IAC5C,MAAM,IAAI,WAAW,CAAC;IACtB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CAC1D"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pi-agent.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pi-agent.d.ts","sourceRoot":"","sources":["../../src/examples/pi-agent.ts"],"names":[],"mappings":""}
@@ -0,0 +1,108 @@
1
+ import { access, mkdir, readFile, rm, symlink, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { seedPiOpenAICodexAuthFromCodex } from "../pi/auth.js";
5
+ import { PiHarnessAdapter } from "../pi/piHarness.js";
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const daimonRoot = path.resolve(__dirname, "../..");
8
+ const runtimeRoot = path.join(daimonRoot, ".runtime", "pi-agent");
9
+ const sharedProjectPath = path.join(runtimeRoot, "shared", "product");
10
+ const piAuthPath = path.join(runtimeRoot, "auth", "auth.json");
11
+ const codexAuthPath = path.join(process.env.HOME ?? "", ".codex", "auth.json");
12
+ const ensureFile = async (filePath) => {
13
+ await access(filePath);
14
+ return readFile(filePath, "utf8");
15
+ };
16
+ const assertContains = (content, expected, filePath) => {
17
+ if (!content.includes(expected)) {
18
+ throw new Error(`${filePath} does not contain expected text: ${expected}`);
19
+ }
20
+ };
21
+ const prepareWorkspace = async (agentId) => {
22
+ const workspacePath = path.join(runtimeRoot, "agents", agentId, "workspace");
23
+ const runtimeHomePath = path.join(runtimeRoot, "agents", agentId, "runtime");
24
+ await mkdir(path.join(workspacePath, "repos"), { recursive: true });
25
+ await mkdir(runtimeHomePath, { recursive: true });
26
+ await writeFile(path.join(workspacePath, "AGENTS.md"), `# ${agentId}\n\nThis workspace was prepared by caller code, not by the Daimon package.\n`);
27
+ await symlink(sharedProjectPath, path.join(workspacePath, "repos", "product"));
28
+ return { workspacePath, runtimeHomePath };
29
+ };
30
+ const setupRuntime = async () => {
31
+ await rm(runtimeRoot, { recursive: true, force: true });
32
+ await mkdir(sharedProjectPath, { recursive: true });
33
+ await writeFile(path.join(sharedProjectPath, "source.txt"), [
34
+ "Daimon E2E source file.",
35
+ "The caller prepares shared files and Daimon runs agent turns.",
36
+ "Expected marker: daimon-e2e"
37
+ ].join("\n"));
38
+ await seedPiOpenAICodexAuthFromCodex({ codexAuthPath, piAuthPath });
39
+ };
40
+ const startHarnessedAgent = async (adapter, input) => {
41
+ const paths = await prepareWorkspace(input.id);
42
+ return adapter.startAgent({
43
+ ...input,
44
+ ...paths
45
+ });
46
+ };
47
+ const run = async () => {
48
+ await setupRuntime();
49
+ const adapter = new PiHarnessAdapter({
50
+ authPath: piAuthPath,
51
+ model: {
52
+ provider: "openai-codex",
53
+ name: process.env.HARNESS_PI_MODEL ?? "gpt-5.4-mini"
54
+ }
55
+ });
56
+ const mapper = await startHarnessedAgent(adapter, {
57
+ id: "mapper",
58
+ name: "Mapper",
59
+ instructions: "You map project inputs into concise markdown notes. Prefer editing files over long chat responses."
60
+ });
61
+ const reviewer = await startHarnessedAgent(adapter, {
62
+ id: "reviewer",
63
+ name: "Reviewer",
64
+ instructions: "You review files written by another harnessed agent and produce a final check note."
65
+ });
66
+ try {
67
+ console.log("started", JSON.stringify([mapper.status(), reviewer.status()], null, 2));
68
+ const mapped = await mapper.wake({
69
+ id: "wake-mapper-1",
70
+ kind: "manual",
71
+ from: "caller",
72
+ text: [
73
+ "Read repos/product/source.txt.",
74
+ "Create repos/product/agent-output/mapper.md.",
75
+ "The file must contain the exact marker MAPPER_OK and the phrase daimon-e2e.",
76
+ "Reply with one short sentence naming the file."
77
+ ].join("\n")
78
+ });
79
+ console.log("mapper", JSON.stringify(mapped, null, 2));
80
+ const reviewed = await reviewer.wake({
81
+ id: "wake-reviewer-1",
82
+ kind: "message",
83
+ from: "mapper",
84
+ text: [
85
+ "Read repos/product/agent-output/mapper.md.",
86
+ "Create repos/product/agent-output/review.md.",
87
+ "The file must contain the exact marker REVIEW_OK and mention MAPPER_OK.",
88
+ "Reply with one short sentence naming the file."
89
+ ].join("\n")
90
+ });
91
+ console.log("reviewer", JSON.stringify(reviewed, null, 2));
92
+ const mapperFile = path.join(sharedProjectPath, "agent-output", "mapper.md");
93
+ const reviewFile = path.join(sharedProjectPath, "agent-output", "review.md");
94
+ assertContains(await ensureFile(mapperFile), "MAPPER_OK", mapperFile);
95
+ assertContains(await ensureFile(mapperFile), "daimon-e2e", mapperFile);
96
+ assertContains(await ensureFile(reviewFile), "REVIEW_OK", reviewFile);
97
+ assertContains(await ensureFile(reviewFile), "MAPPER_OK", reviewFile);
98
+ console.log("final-status", JSON.stringify([mapper.status(), reviewer.status()], null, 2));
99
+ console.log("e2e:pi-agent ok");
100
+ }
101
+ finally {
102
+ await Promise.all([mapper.stop(), reviewer.stop()]);
103
+ }
104
+ };
105
+ run().catch((error) => {
106
+ console.error(error instanceof Error ? error.stack ?? error.message : String(error));
107
+ process.exitCode = 1;
108
+ });
@@ -0,0 +1,5 @@
1
+ export * from "./core/types.js";
2
+ export * from "./pi/auth.js";
3
+ export * from "./pi/modelConfig.js";
4
+ export * from "./pi/piHarness.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./core/types.js";
2
+ export * from "./pi/auth.js";
3
+ export * from "./pi/modelConfig.js";
4
+ export * from "./pi/piHarness.js";
@@ -0,0 +1,20 @@
1
+ export declare function seedPiOpenAICodexAuthFromCodex(input: {
2
+ codexAuthPath: string;
3
+ piAuthPath: string;
4
+ }): Promise<void>;
5
+ export declare function seedPiAnthropicAuthFromClaudeCode(input: {
6
+ claudeCredentialsPath: string;
7
+ piAuthPath: string;
8
+ }): Promise<void>;
9
+ export declare function seedPiApiKeyAuth(input: {
10
+ apiKey: string;
11
+ env?: Record<string, string>;
12
+ piAuthPath: string;
13
+ provider: string;
14
+ }): Promise<void>;
15
+ export declare function createPiOpenAICodexAuthFromCodexToken(input: {
16
+ access: string;
17
+ accountId?: string;
18
+ refresh: string;
19
+ }): Record<string, unknown>;
20
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/pi/auth.ts"],"names":[],"mappings":"AA2DA,wBAAsB,8BAA8B,CAAC,KAAK,EAAE;IAC1D,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAmBhB;AAED,wBAAsB,iCAAiC,CAAC,KAAK,EAAE;IAC7D,qBAAqB,EAAE,MAAM,CAAC;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBhB;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE;IAC5C,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAYhB;AAED,wBAAgB,qCAAqC,CAAC,KAAK,EAAE;IAC3D,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAU1B"}
@@ -0,0 +1,98 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ const decodeJwtPayload = (token) => {
4
+ const payload = token.split(".")[1];
5
+ if (!payload) {
6
+ return null;
7
+ }
8
+ try {
9
+ return JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ };
15
+ const resolveExpires = (accessToken) => {
16
+ const payload = decodeJwtPayload(accessToken);
17
+ const exp = payload?.exp;
18
+ return typeof exp === "number" ? exp * 1000 : Date.now() + 30 * 60 * 1000;
19
+ };
20
+ const readJsonFile = async (filePath) => JSON.parse(await readFile(filePath, "utf8"));
21
+ const readPiAuth = async (piAuthPath) => {
22
+ try {
23
+ return await readJsonFile(piAuthPath);
24
+ }
25
+ catch (error) {
26
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
27
+ return {};
28
+ }
29
+ throw error;
30
+ }
31
+ };
32
+ const writePiAuth = async (piAuthPath, next) => {
33
+ await mkdir(path.dirname(piAuthPath), { recursive: true });
34
+ await writeFile(piAuthPath, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
35
+ };
36
+ export async function seedPiOpenAICodexAuthFromCodex(input) {
37
+ const codex = await readJsonFile(input.codexAuthPath);
38
+ const access = codex.tokens?.access_token;
39
+ const refresh = codex.tokens?.refresh_token;
40
+ const accountId = codex.tokens?.account_id;
41
+ if (!access || !refresh || !accountId) {
42
+ throw new Error(`Codex auth file is missing access_token, refresh_token, or account_id: ${input.codexAuthPath}`);
43
+ }
44
+ const piAuth = await readPiAuth(input.piAuthPath);
45
+ piAuth["openai-codex"] = {
46
+ type: "oauth",
47
+ access,
48
+ refresh,
49
+ expires: resolveExpires(access),
50
+ accountId
51
+ };
52
+ await writePiAuth(input.piAuthPath, piAuth);
53
+ }
54
+ export async function seedPiAnthropicAuthFromClaudeCode(input) {
55
+ const claude = await readJsonFile(input.claudeCredentialsPath);
56
+ const access = claude.claudeAiOauth?.accessToken;
57
+ const refresh = claude.claudeAiOauth?.refreshToken;
58
+ const expires = claude.claudeAiOauth?.expiresAt;
59
+ if (!access || typeof expires !== "number") {
60
+ throw new Error(`Claude Code credentials are missing accessToken or expiresAt: ${input.claudeCredentialsPath}`);
61
+ }
62
+ const piAuth = await readPiAuth(input.piAuthPath);
63
+ piAuth.anthropic = refresh
64
+ ? {
65
+ type: "oauth",
66
+ access,
67
+ refresh,
68
+ expires
69
+ }
70
+ : {
71
+ type: "api_key",
72
+ key: access
73
+ };
74
+ await writePiAuth(input.piAuthPath, piAuth);
75
+ }
76
+ export async function seedPiApiKeyAuth(input) {
77
+ if (input.apiKey.length === 0) {
78
+ throw new Error(`API key for ${input.provider} must not be empty`);
79
+ }
80
+ const piAuth = await readPiAuth(input.piAuthPath);
81
+ piAuth[input.provider] = {
82
+ type: "api_key",
83
+ key: input.apiKey,
84
+ ...(input.env ? { env: input.env } : {})
85
+ };
86
+ await writePiAuth(input.piAuthPath, piAuth);
87
+ }
88
+ export function createPiOpenAICodexAuthFromCodexToken(input) {
89
+ return {
90
+ "openai-codex": {
91
+ type: "oauth",
92
+ access: input.access,
93
+ refresh: input.refresh,
94
+ expires: resolveExpires(input.access),
95
+ ...(input.accountId ? { accountId: input.accountId } : {})
96
+ }
97
+ };
98
+ }
@@ -0,0 +1,37 @@
1
+ import type { HarnessModelSpec } from "../core/types.js";
2
+ type PiApi = "anthropic-messages" | "openai-completions";
3
+ interface PiModelProviderConfig {
4
+ api: PiApi;
5
+ apiKey: string;
6
+ baseUrl: string;
7
+ models: Array<{
8
+ api: PiApi;
9
+ baseUrl: string;
10
+ contextWindow: number;
11
+ cost: {
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ input: number;
15
+ output: number;
16
+ };
17
+ id: string;
18
+ input: Array<"image" | "text">;
19
+ maxTokens: number;
20
+ name: string;
21
+ reasoning: boolean;
22
+ }>;
23
+ }
24
+ export interface PiModelResolution {
25
+ model: {
26
+ name: string;
27
+ provider: string;
28
+ };
29
+ modelsConfig: {
30
+ providers: Record<string, PiModelProviderConfig>;
31
+ };
32
+ }
33
+ export declare const createPiProviderId: (model: HarnessModelSpec) => string;
34
+ export declare const resolvePiHarnessModel: (model: HarnessModelSpec) => PiModelResolution;
35
+ export declare const renderPiModelsConfig: (models: HarnessModelSpec[]) => string;
36
+ export {};
37
+ //# sourceMappingURL=modelConfig.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modelConfig.d.ts","sourceRoot":"","sources":["../../src/pi/modelConfig.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,KAAK,KAAK,GAAG,oBAAoB,GAAG,oBAAoB,CAAC;AAEzD,UAAU,qBAAqB;IAC7B,GAAG,EAAE,KAAK,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,KAAK,CAAC;QACZ,GAAG,EAAE,KAAK,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,aAAa,EAAE,MAAM,CAAC;QACtB,IAAI,EAAE;YACJ,SAAS,EAAE,MAAM,CAAC;YAClB,UAAU,EAAE,MAAM,CAAC;YACnB,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;SAChB,CAAC;QACF,EAAE,EAAE,MAAM,CAAC;QACX,KAAK,EAAE,KAAK,CAAC,OAAO,GAAG,MAAM,CAAC,CAAC;QAC/B,SAAS,EAAE,MAAM,CAAC;QAClB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC,CAAC;CACJ;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,YAAY,EAAE;QACZ,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,qBAAqB,CAAC,CAAC;KAClD,CAAC;CACH;AA2CD,eAAO,MAAM,kBAAkB,GAAI,OAAO,gBAAgB,KAAG,MAG5D,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,OAAO,gBAAgB,KAAG,iBAiD/D,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,gBAAgB,EAAE,KAAG,MAajE,CAAC"}
@@ -0,0 +1,94 @@
1
+ import { createHash } from "node:crypto";
2
+ const stableStringify = (value) => {
3
+ if (Array.isArray(value)) {
4
+ return `[${value.map(stableStringify).join(",")}]`;
5
+ }
6
+ if (value && typeof value === "object") {
7
+ return `{${Object.entries(value)
8
+ .sort(([left], [right]) => left.localeCompare(right))
9
+ .map(([key, item]) => `${JSON.stringify(key)}:${stableStringify(item)}`)
10
+ .join(",")}}`;
11
+ }
12
+ return JSON.stringify(value);
13
+ };
14
+ const slugify = (value) => value
15
+ .toLowerCase()
16
+ .replace(/[^a-z0-9]+/g, "-")
17
+ .replace(/^-+|-+$/g, "");
18
+ const shortHash = (value) => createHash("sha1").update(value).digest("hex").slice(0, 8);
19
+ const endpointApi = (compatibility) => compatibility === "anthropic" ? "anthropic-messages" : "openai-completions";
20
+ const endpointApiKey = (model) => model.auth?.method === "api_key"
21
+ ? `$${model.auth.keyEnv ?? `${model.provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`}`
22
+ : "ollama";
23
+ const assertSupportedEndpointAuth = (model) => {
24
+ const method = model.auth?.method ?? "none";
25
+ if (method === "none" || method === "api_key") {
26
+ return;
27
+ }
28
+ throw new Error(`Pi endpoint models only support none or api_key auth, got ${method}`);
29
+ };
30
+ export const createPiProviderId = (model) => {
31
+ const base = slugify(`${model.provider}-${model.endpoint?.compatibility ?? "builtin"}-${model.name}`);
32
+ return `${base || "model"}-${shortHash(stableStringify(model))}`;
33
+ };
34
+ export const resolvePiHarnessModel = (model) => {
35
+ if (!model.endpoint) {
36
+ return {
37
+ model: {
38
+ name: model.name,
39
+ provider: model.provider === "openai" && model.auth?.method === "codex"
40
+ ? "openai-codex"
41
+ : model.provider
42
+ },
43
+ modelsConfig: { providers: {} }
44
+ };
45
+ }
46
+ assertSupportedEndpointAuth(model);
47
+ const provider = createPiProviderId(model);
48
+ const api = endpointApi(model.endpoint.compatibility);
49
+ return {
50
+ model: {
51
+ name: model.name,
52
+ provider
53
+ },
54
+ modelsConfig: {
55
+ providers: {
56
+ [provider]: {
57
+ api,
58
+ apiKey: endpointApiKey(model),
59
+ baseUrl: model.endpoint.baseUrl,
60
+ models: [
61
+ {
62
+ api,
63
+ baseUrl: model.endpoint.baseUrl,
64
+ contextWindow: 128000,
65
+ cost: {
66
+ cacheRead: 0,
67
+ cacheWrite: 0,
68
+ input: 0,
69
+ output: 0
70
+ },
71
+ id: model.name,
72
+ input: ["text"],
73
+ maxTokens: 16384,
74
+ name: model.name,
75
+ reasoning: false
76
+ }
77
+ ]
78
+ }
79
+ }
80
+ }
81
+ };
82
+ };
83
+ export const renderPiModelsConfig = (models) => {
84
+ const providers = new Map();
85
+ for (const model of models) {
86
+ const resolution = resolvePiHarnessModel(model);
87
+ for (const [provider, config] of Object.entries(resolution.modelsConfig.providers)) {
88
+ providers.set(provider, config);
89
+ }
90
+ }
91
+ return `${JSON.stringify({
92
+ providers: Object.fromEntries([...providers.entries()].sort(([left], [right]) => left.localeCompare(right)))
93
+ }, null, 2)}\n`;
94
+ };
@@ -0,0 +1,19 @@
1
+ import type { AgentHandle, AgentHarnessAdapter, AgentStartInput, HarnessModelSpec } from "../core/types.js";
2
+ export interface PiHarnessOptions {
3
+ authPath: string;
4
+ model?: {
5
+ auth?: HarnessModelSpec["auth"];
6
+ endpoint?: HarnessModelSpec["endpoint"];
7
+ provider: string;
8
+ name: string;
9
+ };
10
+ modelsPath?: string;
11
+ }
12
+ export declare class PiHarnessAdapter implements AgentHarnessAdapter {
13
+ private readonly options;
14
+ private readonly authStorage;
15
+ private readonly modelRegistry;
16
+ constructor(options: PiHarnessOptions);
17
+ startAgent(input: AgentStartInput): Promise<AgentHandle>;
18
+ }
19
+ //# sourceMappingURL=piHarness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"piHarness.d.ts","sourceRoot":"","sources":["../../src/pi/piHarness.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,WAAW,EACX,mBAAmB,EACnB,eAAe,EAEf,gBAAgB,EAGjB,MAAM,kBAAkB,CAAC;AAM1B,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAChC,QAAQ,CAAC,EAAE,gBAAgB,CAAC,UAAU,CAAC,CAAC;QACxC,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AA4HD,qBAAa,gBAAiB,YAAW,mBAAmB;IAI9C,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;gBAEjB,OAAO,EAAE,gBAAgB;IAKhD,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,WAAW,CAAC;CAgC/D"}
@@ -0,0 +1,153 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { AuthStorage, createAgentSession, createExtensionRuntime, ModelRegistry, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
4
+ import { resolvePiHarnessModel } from "./modelConfig.js";
5
+ const createModelRegistry = (authStorage, options) => {
6
+ const registry = options.modelsPath
7
+ ? ModelRegistry.create(authStorage, options.modelsPath)
8
+ : ModelRegistry.inMemory(authStorage);
9
+ if (!options.modelsPath && options.model?.endpoint) {
10
+ const { modelsConfig } = resolvePiHarnessModel(options.model);
11
+ for (const [provider, config] of Object.entries(modelsConfig.providers)) {
12
+ registry.registerProvider(provider, {
13
+ api: config.api,
14
+ apiKey: config.apiKey,
15
+ baseUrl: config.baseUrl,
16
+ models: config.models
17
+ });
18
+ }
19
+ }
20
+ return registry;
21
+ };
22
+ class PiAgentHandle {
23
+ id;
24
+ session;
25
+ state = "idle";
26
+ lastWakeAt;
27
+ lastError;
28
+ constructor(id, session) {
29
+ this.id = id;
30
+ this.session = session;
31
+ }
32
+ async wake(event) {
33
+ const startedAt = Date.now();
34
+ const chunks = [];
35
+ this.state = "running";
36
+ this.lastWakeAt = new Date().toISOString();
37
+ this.lastError = undefined;
38
+ const unsubscribe = this.session.subscribe((piEvent) => {
39
+ if (piEvent.type !== "turn_end") {
40
+ return;
41
+ }
42
+ const message = piEvent.message;
43
+ const content = message.content;
44
+ if (typeof content === "string") {
45
+ chunks.push(content);
46
+ }
47
+ else if (Array.isArray(content)) {
48
+ chunks.push(content
49
+ .filter((item) => {
50
+ const candidate = item;
51
+ return candidate.type === "text" && typeof candidate.text === "string";
52
+ })
53
+ .map((item) => item.text)
54
+ .join(""));
55
+ }
56
+ });
57
+ try {
58
+ await this.session.prompt(formatWakePrompt(event), { expandPromptTemplates: false });
59
+ this.state = "idle";
60
+ return {
61
+ agentId: this.id,
62
+ text: chunks.join("\n").trim(),
63
+ durationMs: Date.now() - startedAt
64
+ };
65
+ }
66
+ catch (error) {
67
+ this.state = "failed";
68
+ this.lastError = error instanceof Error ? error.message : String(error);
69
+ throw error;
70
+ }
71
+ finally {
72
+ unsubscribe();
73
+ }
74
+ }
75
+ status() {
76
+ return {
77
+ agentId: this.id,
78
+ state: this.state,
79
+ lastWakeAt: this.lastWakeAt,
80
+ lastError: this.lastError
81
+ };
82
+ }
83
+ async stop() {
84
+ this.session.dispose();
85
+ this.state = "stopped";
86
+ }
87
+ }
88
+ const formatWakePrompt = (event) => `Wake event:
89
+ - id: ${event.id}
90
+ - kind: ${event.kind}
91
+ - from: ${event.from ?? "operator"}
92
+
93
+ ${event.text}`;
94
+ const createResourceLoader = (input) => {
95
+ const systemPrompt = [
96
+ `You are ${input.name} (${input.id}).`,
97
+ input.instructions,
98
+ "You are running inside a harnessed workspace prepared by the caller.",
99
+ "Use the available coding tools when asked to read, write, edit, or inspect files.",
100
+ "Keep responses brief and report the exact files you created or modified."
101
+ ].join("\n\n");
102
+ return {
103
+ getExtensions: () => ({ extensions: [], errors: [], runtime: createExtensionRuntime() }),
104
+ getSkills: () => ({ skills: [], diagnostics: [] }),
105
+ getPrompts: () => ({ prompts: [], diagnostics: [] }),
106
+ getThemes: () => ({ themes: [], diagnostics: [] }),
107
+ getAgentsFiles: () => ({ agentsFiles: [] }),
108
+ getSystemPrompt: () => systemPrompt,
109
+ getAppendSystemPrompt: () => [],
110
+ extendResources: () => { },
111
+ reload: async () => { }
112
+ };
113
+ };
114
+ export class PiHarnessAdapter {
115
+ options;
116
+ authStorage;
117
+ modelRegistry;
118
+ constructor(options) {
119
+ this.options = options;
120
+ this.authStorage = AuthStorage.create(options.authPath);
121
+ this.modelRegistry = createModelRegistry(this.authStorage, options);
122
+ }
123
+ async startAgent(input) {
124
+ await mkdir(input.runtimeHomePath, { recursive: true });
125
+ await mkdir(input.workspacePath, { recursive: true });
126
+ const modelSpec = this.options.model ?? {
127
+ auth: { method: "codex" },
128
+ provider: "openai",
129
+ name: "gpt-5.4-mini"
130
+ };
131
+ const resolvedModel = resolvePiHarnessModel(modelSpec).model;
132
+ const model = this.modelRegistry.find(resolvedModel.provider, resolvedModel.name);
133
+ if (!model) {
134
+ throw new Error(`Pi model not found: ${resolvedModel.provider}/${resolvedModel.name}`);
135
+ }
136
+ const { session } = await createAgentSession({
137
+ cwd: input.workspacePath,
138
+ agentDir: input.runtimeHomePath,
139
+ authStorage: this.authStorage,
140
+ modelRegistry: this.modelRegistry,
141
+ model,
142
+ thinkingLevel: "off",
143
+ resourceLoader: createResourceLoader(input),
144
+ tools: input.tools ?? ["read", "write", "edit", "bash", "grep", "find", "ls"],
145
+ sessionManager: SessionManager.create(input.workspacePath, path.join(input.runtimeHomePath, "sessions")),
146
+ settingsManager: SettingsManager.inMemory({
147
+ compaction: { enabled: false },
148
+ retry: { enabled: true, maxRetries: 1 }
149
+ })
150
+ });
151
+ return new PiAgentHandle(input.id, session);
152
+ }
153
+ }
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@noopolis/daimon",
3
+ "version": "0.1.0",
4
+ "description": "Noopolis native per-agent runtime harness built on Pi.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "README.md"
18
+ ],
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+ssh://git@github.com/noopolis/daimon.git"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "scripts": {
27
+ "build": "rm -rf dist && tsc --project tsconfig.build.json",
28
+ "clean": "rm -rf dist",
29
+ "prepack": "npm run build",
30
+ "prepublishOnly": "npm run typecheck && npm test",
31
+ "typecheck": "tsc --project tsconfig.json --noEmit",
32
+ "test": "node --import tsx --test \"src/**/*.test.ts\"",
33
+ "e2e:pi-agent": "tsx src/examples/pi-agent.ts"
34
+ },
35
+ "engines": {
36
+ "node": ">=22.19.0"
37
+ },
38
+ "dependencies": {
39
+ "@earendil-works/pi-ai": "^0.79.9",
40
+ "@earendil-works/pi-coding-agent": "^0.79.9"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^24.12.4",
44
+ "tsx": "^4.21.0",
45
+ "typescript": "^5.9.3"
46
+ }
47
+ }