@ai-ecoverse/slicc-agent 2.0.0 → 2.2.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 (48) hide show
  1. package/README.md +39 -0
  2. package/dist/agent.js +2 -1
  3. package/dist/agent.js.map +2 -2
  4. package/dist/control.js +9 -1
  5. package/dist/control.js.map +2 -2
  6. package/dist/index.js +6 -1
  7. package/dist/index.js.map +2 -2
  8. package/dist/kernel/client.js +1 -0
  9. package/dist/kernel/client.js.map +7 -0
  10. package/dist/kernel/env.js +204 -0
  11. package/dist/kernel/env.js.map +7 -0
  12. package/dist/kernel/exec.js +115 -0
  13. package/dist/kernel/exec.js.map +7 -0
  14. package/dist/kernel/files.js +153 -0
  15. package/dist/kernel/files.js.map +7 -0
  16. package/dist/kernel/paths.js +31 -0
  17. package/dist/kernel/paths.js.map +7 -0
  18. package/dist/kernel/watch.js +71 -0
  19. package/dist/kernel/watch.js.map +7 -0
  20. package/dist/owner.js +32 -9
  21. package/dist/owner.js.map +2 -2
  22. package/dist/services.js.map +2 -2
  23. package/dist/spectrum/agent-port.js +156 -0
  24. package/dist/spectrum/agent-port.js.map +7 -0
  25. package/dist/spectrum/emitter.js +20 -0
  26. package/dist/spectrum/emitter.js.map +7 -0
  27. package/dist/spectrum/index.js +17 -0
  28. package/dist/spectrum/index.js.map +7 -0
  29. package/dist/spectrum/messages.js +161 -0
  30. package/dist/spectrum/messages.js.map +7 -0
  31. package/dist/types/agent.d.ts +1 -0
  32. package/dist/types/index.d.ts +4 -2
  33. package/dist/types/kernel/client.d.ts +39 -0
  34. package/dist/types/kernel/env.d.ts +57 -0
  35. package/dist/types/kernel/exec.d.ts +5 -0
  36. package/dist/types/kernel/files.d.ts +34 -0
  37. package/dist/types/kernel/paths.d.ts +4 -0
  38. package/dist/types/kernel/watch.d.ts +10 -0
  39. package/dist/types/owner.d.ts +6 -2
  40. package/dist/types/services.d.ts +9 -0
  41. package/dist/types/spectrum/agent-port.d.ts +32 -0
  42. package/dist/types/spectrum/emitter.d.ts +7 -0
  43. package/dist/types/spectrum/index.d.ts +7 -0
  44. package/dist/types/spectrum/messages.d.ts +37 -0
  45. package/dist/types/worker.d.ts +1 -0
  46. package/dist/worker.js +9 -0
  47. package/dist/worker.js.map +2 -2
  48. package/package.json +13 -2
package/README.md CHANGED
@@ -31,6 +31,45 @@ await agent.prompt('What is in /home?');
31
31
  - `hostAgent(agent)` serves the agent with pi's protocol: a pi-server with one session, `agent`, whose Chord services are `slicc.agent.control` (send with `whenBusy` `steer`, `followUp` or `reject`, wait, withdraw, abort, compact) and `slicc.agent.transcript` (the conversation's durable view as replicated state). `serveConnections(self, host)` hands it every `MessagePort` the page sends.
32
32
  - `startAgent({ worker })` takes the Web Lock `slicc-agent`, so one tab at a time owns the agent worker, and waits while another tab holds it. `connect()` returns an `AgentConnection` (`control`, `transcript`, `prompt()`), `restart()` replaces the worker (durable resumes from SQLite), and `release()` stops it and lets the next tab take over.
33
33
 
34
+ ### Bash and files on slicc-kernel
35
+
36
+ The page owns the one kernel (plan decision D3), and the agent worker attaches to it as a second client ([slicc-kernel#66](https://github.com/ai-ecoverse/slicc-kernel/issues/66)), so a terminal's `ps` and `kill` reach whatever the agent started.
37
+
38
+ ```js
39
+ const owner = await startAgent({ worker, kernel: { connect: () => kernel.connect() } });
40
+ ```
41
+
42
+ ```js
43
+ import { attachKernel } from '@ai-ecoverse/slicc-kernel';
44
+ import { CodingTools } from '@earendil-works/pi-durable/tools';
45
+ import { kernelEnvironment, kernelPort } from '@ai-ecoverse/slicc-agent';
46
+
47
+ const client = await attachKernel(await kernelPort(self));
48
+ registry.install(CodingTools);
49
+ const agent = await openAgent({ models, model, storage, registry, env: kernelEnvironment(client) });
50
+ ```
51
+
52
+ `SliccKernelEnv` implements pi durable's `ExecutionEnv` on the kernel client and passes pi's environment conformance suite:
53
+ - string commands run as `bash -c`, each in its own process group; timeouts and aborts kill the group;
54
+ - output past the spill limits goes to `/tmp/slicc-agent-output-*.log`;
55
+ - files and directories go through the kernel's OPFS view and metadata sidecar;
56
+ - `watch()` polls until the kernel offers file watching.
57
+
58
+ A conversation without a `cwd` works in `/home`.
59
+
60
+ ### In slicc-spectrum
61
+
62
+ `@ai-ecoverse/slicc-agent/spectrum` turns an `AgentConnection` into slicc-spectrum's `AgentPort`, so `<slicc-app>` shows the real agent:
63
+
64
+ ```js
65
+ import { createAgentModel } from '@ai-ecoverse/slicc-agent/spectrum';
66
+
67
+ const agent = await owner.connect();
68
+ app.model = { ...createKernelModel({ kernel, root }), ...createAgentModel(agent) };
69
+ ```
70
+
71
+ The adapter keeps the replicated transcript and maps it on every update. User, assistant and compaction entries become messages; tool results fill in the tool calls they answer; the generation in flight shows as a streaming message; retries and running compactions show as system messages. `busy()` and `queue()` read durable's live and inbox documents. A plain send queues a follow-up, and a steer send steers, as slicc-spectrum's composer asks today. There is one cone so far; scoops, the freezer, licks and questions come later. slicc-spectrum is a peer dependency (≥ 1.7.0) used for types only.
72
+
34
73
  pi-server 1.0.4 calls `unref()` on a `setTimeout` handle when it accepts a connection, which browsers don't have. `PortListener` wraps that one call so its timers get a no-op `unref`.
35
74
 
36
75
  ## Develop
package/dist/agent.js CHANGED
@@ -25,7 +25,8 @@ async function openAgent(options) {
25
25
  {
26
26
  models: options.models,
27
27
  registry: options.registry ?? createRegistry(),
28
- ...options.settings ? { settings: options.settings } : {}
28
+ ...options.settings ? { settings: options.settings } : {},
29
+ ...options.env ? { env: options.env } : {}
29
30
  },
30
31
  context
31
32
  );
package/dist/agent.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/agent.ts"],
4
- "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport {\n AssistantEntry,\n type Conversation,\n createRegistry,\n type EntryId,\n Harness,\n type HarnessOptions,\n MemoryStorage,\n type ModelRef,\n type Registry,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nexport interface AgentOptions {\n models: Models;\n model: ModelRef;\n storage?: Storage;\n registry?: Registry;\n settings?: HarnessOptions['settings'];\n context?: Context;\n}\n\nexport interface Agent {\n readonly harness: Harness;\n readonly root: Conversation;\n prompt(text: string): Promise<string>;\n close(): Promise<void>;\n}\n\ninterface Block {\n type: string;\n text?: string;\n}\n\nexport function answerText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return (content as Block[])\n .filter((block) => block.type === 'text')\n .map((block) => block.text ?? '')\n .join('');\n}\n\nasync function prompt(root: Conversation, text: string, context: Context): Promise<string> {\n const submission = await root.submit({ type: 'input', content: text }, context);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) throw new Error(`unanswered: ${reason}`);\n const entry = await root.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return answerText(entry?.model?.[0]?.content);\n}\n\nexport async function openAgent(options: AgentOptions): Promise<Agent> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const harness = await Harness.open(\n options.storage ?? new MemoryStorage(),\n {\n models: options.models,\n registry: options.registry ?? createRegistry(),\n ...(options.settings ? { settings: options.settings } : {}),\n },\n context\n );\n const root = await harness.root(context, { agent: { model: options.model } });\n harness.resume();\n return {\n harness,\n root,\n prompt: (text) => prompt(root, text, context),\n close: () => harness.close(context),\n };\n}\n"],
5
- "mappings": "AACA,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,OAIK;AAuBA,SAAS,WAAW,SAA0B;AACnD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,SAAQ,QACL,OAAO,CAAC,UAAU,MAAM,SAAS,MAAM,EACvC,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,EAC/B,KAAK,EAAE;AACZ;AAEA,eAAe,OAAO,MAAoB,MAAc,SAAmC;AACzF,QAAM,aAAa,MAAM,KAAK,OAAO,EAAE,MAAM,SAAS,SAAS,KAAK,GAAG,OAAO;AAC9E,QAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,MAAI,WAAW,OAAW,OAAM,IAAI,MAAM,eAAe,MAAM,EAAE;AACjE,QAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACjF,SAAO,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO;AAC9C;AAEA,eAAsB,UAAU,SAAuC;AACrE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ,WAAW,IAAI,cAAc;AAAA,IACrC;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ,YAAY,eAAe;AAAA,MAC7C,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IAC3D;AAAA,IACA;AAAA,EACF;AACA,QAAM,OAAO,MAAM,QAAQ,KAAK,SAAS,EAAE,OAAO,EAAE,OAAO,QAAQ,MAAM,EAAE,CAAC;AAC5E,UAAQ,OAAO;AACf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ,CAAC,SAAS,OAAO,MAAM,MAAM,OAAO;AAAA,IAC5C,OAAO,MAAM,QAAQ,MAAM,OAAO;AAAA,EACpC;AACF;",
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport type { Models } from '@earendil-works/pi-ai/models';\nimport {\n AssistantEntry,\n type Conversation,\n createRegistry,\n type EntryId,\n Harness,\n type HarnessOptions,\n MemoryStorage,\n type ModelRef,\n type Registry,\n type Storage,\n} from '@earendil-works/pi-durable';\n\nexport interface AgentOptions {\n models: Models;\n model: ModelRef;\n storage?: Storage;\n registry?: Registry;\n settings?: HarnessOptions['settings'];\n env?: HarnessOptions['env'];\n context?: Context;\n}\n\nexport interface Agent {\n readonly harness: Harness;\n readonly root: Conversation;\n prompt(text: string): Promise<string>;\n close(): Promise<void>;\n}\n\ninterface Block {\n type: string;\n text?: string;\n}\n\nexport function answerText(content: unknown): string {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return '';\n return (content as Block[])\n .filter((block) => block.type === 'text')\n .map((block) => block.text ?? '')\n .join('');\n}\n\nasync function prompt(root: Conversation, text: string, context: Context): Promise<string> {\n const submission = await root.submit({ type: 'input', content: text }, context);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) throw new Error(`unanswered: ${reason}`);\n const entry = await root.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return answerText(entry?.model?.[0]?.content);\n}\n\nexport async function openAgent(options: AgentOptions): Promise<Agent> {\n const context = options.context ?? BACKGROUND_CONTEXT;\n const harness = await Harness.open(\n options.storage ?? new MemoryStorage(),\n {\n models: options.models,\n registry: options.registry ?? createRegistry(),\n ...(options.settings ? { settings: options.settings } : {}),\n ...(options.env ? { env: options.env } : {}),\n },\n context\n );\n const root = await harness.root(context, { agent: { model: options.model } });\n harness.resume();\n return {\n harness,\n root,\n prompt: (text) => prompt(root, text, context),\n close: () => harness.close(context),\n };\n}\n"],
5
+ "mappings": "AACA,SAAS,0BAA0B;AAEnC;AAAA,EACE;AAAA,EAEA;AAAA,EAEA;AAAA,EAEA;AAAA,OAIK;AAwBA,SAAS,WAAW,SAA0B;AACnD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,SAAQ,QACL,OAAO,CAAC,UAAU,MAAM,SAAS,MAAM,EACvC,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,EAC/B,KAAK,EAAE;AACZ;AAEA,eAAe,OAAO,MAAoB,MAAc,SAAmC;AACzF,QAAM,aAAa,MAAM,KAAK,OAAO,EAAE,MAAM,SAAS,SAAS,KAAK,GAAG,OAAO;AAC9E,QAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,QAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,MAAI,WAAW,OAAW,OAAM,IAAI,MAAM,eAAe,MAAM,EAAE;AACjE,QAAM,QAAQ,MAAM,KAAK,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACjF,SAAO,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO;AAC9C;AAEA,eAAsB,UAAU,SAAuC;AACrE,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,UAAU,MAAM,QAAQ;AAAA,IAC5B,QAAQ,WAAW,IAAI,cAAc;AAAA,IACrC;AAAA,MACE,QAAQ,QAAQ;AAAA,MAChB,UAAU,QAAQ,YAAY,eAAe;AAAA,MAC7C,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,MACzD,GAAI,QAAQ,MAAM,EAAE,KAAK,QAAQ,IAAI,IAAI,CAAC;AAAA,IAC5C;AAAA,IACA;AAAA,EACF;AACA,QAAM,OAAO,MAAM,QAAQ,KAAK,SAAS,EAAE,OAAO,EAAE,OAAO,QAAQ,MAAM,EAAE,CAAC;AAC5E,UAAQ,OAAO;AACf,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ,CAAC,SAAS,OAAO,MAAM,MAAM,OAAO;AAAA,IAC5C,OAAO,MAAM,QAAQ,MAAM,OAAO;AAAA,EACpC;AACF;",
6
6
  "names": []
7
7
  }
package/dist/control.js CHANGED
@@ -51,7 +51,15 @@ function createAgentControl(harness, conversation) {
51
51
  return { outcome: result === "not_found" ? "not_found" : "already_placed" };
52
52
  },
53
53
  abort: (context) => conversation.abort(context),
54
- compact: (instructions, context) => accepted(() => conversation.compact(instructions ?? void 0, context))
54
+ compact: (instructions, context) => accepted(() => conversation.compact(instructions ?? void 0, context)),
55
+ reset: (handoff, context) => conversation.reset(handoff ?? void 0, context),
56
+ configure: (change, context) => conversation.configure(
57
+ {
58
+ ...change.model ? { model: change.model } : {},
59
+ ...change.thinkingLevel ? { thinkingLevel: change.thinkingLevel } : {}
60
+ },
61
+ context
62
+ )
55
63
  };
56
64
  }
57
65
  export {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/control.ts"],
4
- "sourcesContent": ["import {\n AssistantEntry,\n type Conversation,\n ConversationBusy,\n type EntryId,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport { answerText } from './agent.ts';\nimport type { AgentControl, OperationError, SendResponse } from './services.ts';\n\nexport function submissionId(value: string): SubmissionId | undefined {\n const id = Number(value);\n return /^[1-9][0-9]*$/.test(value) && Number.isSafeInteger(id) ? (id as SubmissionId) : undefined;\n}\n\nfunction operationError(error: unknown): OperationError {\n if (error instanceof ConversationBusy) return { code: 'busy', message: error.message };\n return { code: 'failed', message: error instanceof Error ? error.message : String(error) };\n}\n\nasync function accepted(operation: () => Promise<{ id: number } | number>): Promise<SendResponse> {\n try {\n const result = await operation();\n const id = typeof result === 'number' ? result : result.id;\n return { accepted: true, submissionId: String(id), error: null };\n } catch (error) {\n return { accepted: false, submissionId: null, error: operationError(error) };\n }\n}\n\nexport function createAgentControl(harness: Harness, conversation: Conversation): AgentControl {\n return {\n send: (request, context) =>\n accepted(() =>\n conversation.submit(\n {\n type: 'input',\n content: request.text,\n whenBusy: request.whenBusy,\n ...(request.requestId ? { requestId: request.requestId } : {}),\n },\n context\n )\n ),\n async wait(id, context) {\n const parsed = submissionId(id);\n const submission =\n parsed === undefined ? undefined : await harness.submission(parsed, context);\n if (!submission) throw new Error(`unknown submission ${id}`);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) return { status: 'unanswered', text: null, reason: String(reason) };\n const entry = await harness.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return { status: 'done', text: answerText(entry?.model?.[0]?.content), reason: null };\n },\n async withdraw(id, context) {\n const parsed = submissionId(id);\n if (parsed === undefined) return { outcome: 'not_found' };\n const result = await harness.abortSubmission(parsed, context, conversation.id);\n if (result === 'aborted') return { outcome: 'withdrawn' };\n return { outcome: result === 'not_found' ? 'not_found' : 'already_placed' };\n },\n abort: (context) => conversation.abort(context),\n compact: (instructions, context) =>\n accepted(() => conversation.compact(instructions ?? undefined, context)),\n };\n}\n"],
5
- "mappings": "AAAA;AAAA,EACE;AAAA,EAEA;AAAA,OAIK;AACP,SAAS,kBAAkB;AAGpB,SAAS,aAAa,OAAyC;AACpE,QAAM,KAAK,OAAO,KAAK;AACvB,SAAO,gBAAgB,KAAK,KAAK,KAAK,OAAO,cAAc,EAAE,IAAK,KAAsB;AAC1F;AAEA,SAAS,eAAe,OAAgC;AACtD,MAAI,iBAAiB,iBAAkB,QAAO,EAAE,MAAM,QAAQ,SAAS,MAAM,QAAQ;AACrF,SAAO,EAAE,MAAM,UAAU,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAC3F;AAEA,eAAe,SAAS,WAA0E;AAChG,MAAI;AACF,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,KAAK,OAAO,WAAW,WAAW,SAAS,OAAO;AACxD,WAAO,EAAE,UAAU,MAAM,cAAc,OAAO,EAAE,GAAG,OAAO,KAAK;AAAA,EACjE,SAAS,OAAO;AACd,WAAO,EAAE,UAAU,OAAO,cAAc,MAAM,OAAO,eAAe,KAAK,EAAE;AAAA,EAC7E;AACF;AAEO,SAAS,mBAAmB,SAAkB,cAA0C;AAC7F,SAAO;AAAA,IACL,MAAM,CAAC,SAAS,YACd;AAAA,MAAS,MACP,aAAa;AAAA,QACX;AAAA,UACE,MAAM;AAAA,UACN,SAAS,QAAQ;AAAA,UACjB,UAAU,QAAQ;AAAA,UAClB,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;AAAA,QAC9D;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IACF,MAAM,KAAK,IAAI,SAAS;AACtB,YAAM,SAAS,aAAa,EAAE;AAC9B,YAAM,aACJ,WAAW,SAAY,SAAY,MAAM,QAAQ,WAAW,QAAQ,OAAO;AAC7E,UAAI,CAAC,WAAY,OAAM,IAAI,MAAM,sBAAsB,EAAE,EAAE;AAC3D,YAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,UAAI,WAAW,OAAW,QAAO,EAAE,QAAQ,cAAc,MAAM,MAAM,QAAQ,OAAO,MAAM,EAAE;AAC5F,YAAM,QAAQ,MAAM,QAAQ,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACpF,aAAO,EAAE,QAAQ,QAAQ,MAAM,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO,GAAG,QAAQ,KAAK;AAAA,IACtF;AAAA,IACA,MAAM,SAAS,IAAI,SAAS;AAC1B,YAAM,SAAS,aAAa,EAAE;AAC9B,UAAI,WAAW,OAAW,QAAO,EAAE,SAAS,YAAY;AACxD,YAAM,SAAS,MAAM,QAAQ,gBAAgB,QAAQ,SAAS,aAAa,EAAE;AAC7E,UAAI,WAAW,UAAW,QAAO,EAAE,SAAS,YAAY;AACxD,aAAO,EAAE,SAAS,WAAW,cAAc,cAAc,iBAAiB;AAAA,IAC5E;AAAA,IACA,OAAO,CAAC,YAAY,aAAa,MAAM,OAAO;AAAA,IAC9C,SAAS,CAAC,cAAc,YACtB,SAAS,MAAM,aAAa,QAAQ,gBAAgB,QAAW,OAAO,CAAC;AAAA,EAC3E;AACF;",
4
+ "sourcesContent": ["import {\n AssistantEntry,\n type Conversation,\n ConversationBusy,\n type EntryId,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport { answerText } from './agent.ts';\nimport type { AgentControl, OperationError, SendResponse } from './services.ts';\n\nexport function submissionId(value: string): SubmissionId | undefined {\n const id = Number(value);\n return /^[1-9][0-9]*$/.test(value) && Number.isSafeInteger(id) ? (id as SubmissionId) : undefined;\n}\n\nfunction operationError(error: unknown): OperationError {\n if (error instanceof ConversationBusy) return { code: 'busy', message: error.message };\n return { code: 'failed', message: error instanceof Error ? error.message : String(error) };\n}\n\nasync function accepted(operation: () => Promise<{ id: number } | number>): Promise<SendResponse> {\n try {\n const result = await operation();\n const id = typeof result === 'number' ? result : result.id;\n return { accepted: true, submissionId: String(id), error: null };\n } catch (error) {\n return { accepted: false, submissionId: null, error: operationError(error) };\n }\n}\n\nexport function createAgentControl(harness: Harness, conversation: Conversation): AgentControl {\n return {\n send: (request, context) =>\n accepted(() =>\n conversation.submit(\n {\n type: 'input',\n content: request.text,\n whenBusy: request.whenBusy,\n ...(request.requestId ? { requestId: request.requestId } : {}),\n },\n context\n )\n ),\n async wait(id, context) {\n const parsed = submissionId(id);\n const submission =\n parsed === undefined ? undefined : await harness.submission(parsed, context);\n if (!submission) throw new Error(`unknown submission ${id}`);\n const settled = await submission.wait(context);\n const { answer, reason } = settled as { answer?: EntryId; reason?: string };\n if (answer === undefined) return { status: 'unanswered', text: null, reason: String(reason) };\n const entry = await harness.commit((tx) => tx.entry(AssistantEntry, answer), context);\n return { status: 'done', text: answerText(entry?.model?.[0]?.content), reason: null };\n },\n async withdraw(id, context) {\n const parsed = submissionId(id);\n if (parsed === undefined) return { outcome: 'not_found' };\n const result = await harness.abortSubmission(parsed, context, conversation.id);\n if (result === 'aborted') return { outcome: 'withdrawn' };\n return { outcome: result === 'not_found' ? 'not_found' : 'already_placed' };\n },\n abort: (context) => conversation.abort(context),\n compact: (instructions, context) =>\n accepted(() => conversation.compact(instructions ?? undefined, context)),\n reset: (handoff, context) => conversation.reset(handoff ?? undefined, context),\n configure: (change, context) =>\n conversation.configure(\n {\n ...(change.model ? { model: change.model } : {}),\n ...(change.thinkingLevel ? { thinkingLevel: change.thinkingLevel } : {}),\n },\n context\n ),\n };\n}\n"],
5
+ "mappings": "AAAA;AAAA,EACE;AAAA,EAEA;AAAA,OAIK;AACP,SAAS,kBAAkB;AAGpB,SAAS,aAAa,OAAyC;AACpE,QAAM,KAAK,OAAO,KAAK;AACvB,SAAO,gBAAgB,KAAK,KAAK,KAAK,OAAO,cAAc,EAAE,IAAK,KAAsB;AAC1F;AAEA,SAAS,eAAe,OAAgC;AACtD,MAAI,iBAAiB,iBAAkB,QAAO,EAAE,MAAM,QAAQ,SAAS,MAAM,QAAQ;AACrF,SAAO,EAAE,MAAM,UAAU,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;AAC3F;AAEA,eAAe,SAAS,WAA0E;AAChG,MAAI;AACF,UAAM,SAAS,MAAM,UAAU;AAC/B,UAAM,KAAK,OAAO,WAAW,WAAW,SAAS,OAAO;AACxD,WAAO,EAAE,UAAU,MAAM,cAAc,OAAO,EAAE,GAAG,OAAO,KAAK;AAAA,EACjE,SAAS,OAAO;AACd,WAAO,EAAE,UAAU,OAAO,cAAc,MAAM,OAAO,eAAe,KAAK,EAAE;AAAA,EAC7E;AACF;AAEO,SAAS,mBAAmB,SAAkB,cAA0C;AAC7F,SAAO;AAAA,IACL,MAAM,CAAC,SAAS,YACd;AAAA,MAAS,MACP,aAAa;AAAA,QACX;AAAA,UACE,MAAM;AAAA,UACN,SAAS,QAAQ;AAAA,UACjB,UAAU,QAAQ;AAAA,UAClB,GAAI,QAAQ,YAAY,EAAE,WAAW,QAAQ,UAAU,IAAI,CAAC;AAAA,QAC9D;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAAA,IACF,MAAM,KAAK,IAAI,SAAS;AACtB,YAAM,SAAS,aAAa,EAAE;AAC9B,YAAM,aACJ,WAAW,SAAY,SAAY,MAAM,QAAQ,WAAW,QAAQ,OAAO;AAC7E,UAAI,CAAC,WAAY,OAAM,IAAI,MAAM,sBAAsB,EAAE,EAAE;AAC3D,YAAM,UAAU,MAAM,WAAW,KAAK,OAAO;AAC7C,YAAM,EAAE,QAAQ,OAAO,IAAI;AAC3B,UAAI,WAAW,OAAW,QAAO,EAAE,QAAQ,cAAc,MAAM,MAAM,QAAQ,OAAO,MAAM,EAAE;AAC5F,YAAM,QAAQ,MAAM,QAAQ,OAAO,CAAC,OAAO,GAAG,MAAM,gBAAgB,MAAM,GAAG,OAAO;AACpF,aAAO,EAAE,QAAQ,QAAQ,MAAM,WAAW,OAAO,QAAQ,CAAC,GAAG,OAAO,GAAG,QAAQ,KAAK;AAAA,IACtF;AAAA,IACA,MAAM,SAAS,IAAI,SAAS;AAC1B,YAAM,SAAS,aAAa,EAAE;AAC9B,UAAI,WAAW,OAAW,QAAO,EAAE,SAAS,YAAY;AACxD,YAAM,SAAS,MAAM,QAAQ,gBAAgB,QAAQ,SAAS,aAAa,EAAE;AAC7E,UAAI,WAAW,UAAW,QAAO,EAAE,SAAS,YAAY;AACxD,aAAO,EAAE,SAAS,WAAW,cAAc,cAAc,iBAAiB;AAAA,IAC5E;AAAA,IACA,OAAO,CAAC,YAAY,aAAa,MAAM,OAAO;AAAA,IAC9C,SAAS,CAAC,cAAc,YACtB,SAAS,MAAM,aAAa,QAAQ,gBAAgB,QAAW,OAAO,CAAC;AAAA,IACzE,OAAO,CAAC,SAAS,YAAY,aAAa,MAAM,WAAW,QAAW,OAAO;AAAA,IAC7E,WAAW,CAAC,QAAQ,YAClB,aAAa;AAAA,MACX;AAAA,QACE,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,QAC9C,GAAI,OAAO,gBAAgB,EAAE,eAAe,OAAO,cAAc,IAAI,CAAC;AAAA,MACxE;AAAA,MACA;AAAA,IACF;AAAA,EACJ;AACF;",
6
6
  "names": []
7
7
  }
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { answerText, openAgent } from "./agent.js";
2
2
  import { connectAgent } from "./client.js";
3
3
  import { createAgentControl } from "./control.js";
4
4
  import { hostAgent } from "./host.js";
5
+ import { HOME, kernelEnvironment, SliccKernelEnv } from "./kernel/env.js";
5
6
  import {
6
7
  AGENT_LOCK,
7
8
  startAgent
@@ -19,7 +20,7 @@ import {
19
20
  WasmSqliteDatabase
20
21
  } from "./sqlite.js";
21
22
  import { PortChannel, PortListener, withUnrefTimers } from "./wire.js";
22
- import { serveConnections } from "./worker.js";
23
+ import { kernelPort, serveConnections } from "./worker.js";
23
24
  export {
24
25
  AGENT_DIRECTORY,
25
26
  AGENT_LOCK,
@@ -27,13 +28,17 @@ export {
27
28
  AgentControl,
28
29
  AgentSessions,
29
30
  AgentTranscript,
31
+ HOME,
30
32
  PortChannel,
31
33
  PortListener,
34
+ SliccKernelEnv,
32
35
  WasmSqliteDatabase,
33
36
  answerText,
34
37
  connectAgent,
35
38
  createAgentControl,
36
39
  hostAgent,
40
+ kernelEnvironment,
41
+ kernelPort,
37
42
  openAgent,
38
43
  openMemorySqliteStorage,
39
44
  openOpfsSqliteStorage,
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["export { type Agent, type AgentOptions, answerText, openAgent } from './agent.ts';\nexport { type AgentConnection, connectAgent } from './client.ts';\nexport { createAgentControl } from './control.ts';\nexport { type AgentHost, hostAgent } from './host.ts';\nexport {\n AGENT_LOCK,\n type AgentOwner,\n type AgentWorker,\n type OwnerOptions,\n startAgent,\n} from './owner.ts';\nexport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n AgentTranscript,\n type OperationError,\n type PromptResult,\n type SendMode,\n type SendRequest,\n type SendResponse,\n} from './services.ts';\nexport {\n AGENT_DIRECTORY,\n type OpfsSqliteOptions,\n openMemorySqliteStorage,\n openOpfsSqliteStorage,\n WasmSqliteDatabase,\n} from './sqlite.ts';\nexport { PortChannel, type PortEndpoint, PortListener, withUnrefTimers } from './wire.ts';\nexport { serveConnections, type WorkerScope } from './worker.ts';\n"],
5
- "mappings": "AAAA,SAAwC,YAAY,iBAAiB;AACrE,SAA+B,oBAAoB;AACnD,SAAS,0BAA0B;AACnC,SAAyB,iBAAiB;AAC1C;AAAA,EACE;AAAA,EAIA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAgC,cAAc,uBAAuB;AAC9E,SAAS,wBAA0C;",
4
+ "sourcesContent": ["export { type Agent, type AgentOptions, answerText, openAgent } from './agent.ts';\nexport { type AgentConnection, connectAgent } from './client.ts';\nexport { createAgentControl } from './control.ts';\nexport { type AgentHost, hostAgent } from './host.ts';\nexport type {\n KernelClient,\n KernelFs,\n KernelProcess,\n KernelStat,\n SpawnOptions,\n} from './kernel/client.ts';\nexport { HOME, type KernelEnvOptions, kernelEnvironment, SliccKernelEnv } from './kernel/env.ts';\nexport {\n AGENT_LOCK,\n type AgentOwner,\n type AgentWorker,\n type OwnerOptions,\n startAgent,\n} from './owner.ts';\nexport {\n AGENT_SESSION,\n AgentControl,\n AgentSessions,\n type AgentSettingsChange,\n AgentTranscript,\n type OperationError,\n type PromptResult,\n type SendMode,\n type SendRequest,\n type SendResponse,\n} from './services.ts';\nexport {\n AGENT_DIRECTORY,\n type OpfsSqliteOptions,\n openMemorySqliteStorage,\n openOpfsSqliteStorage,\n WasmSqliteDatabase,\n} from './sqlite.ts';\nexport { PortChannel, type PortEndpoint, PortListener, withUnrefTimers } from './wire.ts';\nexport { kernelPort, serveConnections, type WorkerScope } from './worker.ts';\n"],
5
+ "mappings": "AAAA,SAAwC,YAAY,iBAAiB;AACrE,SAA+B,oBAAoB;AACnD,SAAS,0BAA0B;AACnC,SAAyB,iBAAiB;AAQ1C,SAAS,MAA6B,mBAAmB,sBAAsB;AAC/E;AAAA,EACE;AAAA,EAIA;AAAA,OACK;AACP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OAMK;AACP;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,aAAgC,cAAc,uBAAuB;AAC9E,SAAS,YAAY,wBAA0C;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,204 @@
1
+ import {
2
+ err,
3
+ FileError,
4
+ ok
5
+ } from "@earendil-works/pi-durable/env";
6
+ import { execute } from "./exec.js";
7
+ import { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from "./files.js";
8
+ import { dirname, normalize, resolve } from "./paths.js";
9
+ import { pollWatch } from "./watch.js";
10
+ class SliccKernelEnv {
11
+ id;
12
+ cwd;
13
+ #client;
14
+ #watchers = /* @__PURE__ */ new Set();
15
+ #poll;
16
+ constructor(client, options) {
17
+ this.#client = client;
18
+ this.cwd = options.cwd;
19
+ this.id = options.id ?? "slicc-kernel";
20
+ this.#poll = options.watch ?? {};
21
+ }
22
+ #path(path) {
23
+ return resolve(this.cwd, path);
24
+ }
25
+ get #fs() {
26
+ return this.#client.fs;
27
+ }
28
+ async absolutePath(path) {
29
+ return ok(this.#path(path));
30
+ }
31
+ async joinPath(parts) {
32
+ return ok(normalize(parts.join("/")));
33
+ }
34
+ readTextFile(path, context) {
35
+ const resolved = this.#path(path);
36
+ return attempt(context, resolved, () => this.#fs.readText(resolved));
37
+ }
38
+ readBinaryFile(path, context) {
39
+ const resolved = this.#path(path);
40
+ return attempt(context, resolved, () => this.#fs.readFile(resolved));
41
+ }
42
+ async openTextLineReader(path, context) {
43
+ const text = await this.readTextFile(path, context);
44
+ return text.ok ? ok(new SnapshotLines(text.value)) : text;
45
+ }
46
+ async readTextLines(path, options, context) {
47
+ if (options?.maxLines !== void 0 && options.maxLines <= 0) return ok([]);
48
+ const text = await this.readTextFile(path, context);
49
+ if (!text.ok) return text;
50
+ const reader = new SnapshotLines(text.value);
51
+ const lines = [];
52
+ for (let line = await reader.readLine(); line.ok && line.value; line = await reader.readLine()) {
53
+ if (options?.maxLines !== void 0 && lines.length >= options.maxLines) break;
54
+ lines.push(line.value.text);
55
+ }
56
+ return ok(lines);
57
+ }
58
+ openBinaryReader(path, options, context) {
59
+ const resolved = this.#path(path);
60
+ return attempt(context, resolved, async () => {
61
+ if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {
62
+ throw new FileError("invalid", "Refusing to follow a symbolic link", resolved);
63
+ }
64
+ const stat = await this.#fs.stat(resolved);
65
+ if (stat.isDirectory) throw new FileError("is_directory", "Is a directory", resolved);
66
+ if (!stat.isFile) throw new FileError("invalid", "Not a regular file", resolved);
67
+ return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));
68
+ });
69
+ }
70
+ writeFile(path, content, context) {
71
+ const resolved = this.#path(path);
72
+ return attempt(context, resolved, async () => {
73
+ await this.#fs.mkdir(dirname(resolved));
74
+ await this.#fs.writeFile(resolved, content);
75
+ });
76
+ }
77
+ appendFile(path, content, context) {
78
+ const resolved = this.#path(path);
79
+ return attempt(context, resolved, async () => {
80
+ const before = await this.#fs.exists(resolved) ? await this.#fs.readFile(resolved) : new Uint8Array();
81
+ const added = typeof content === "string" ? new TextEncoder().encode(content) : content;
82
+ const all = new Uint8Array(before.length + added.length);
83
+ all.set(before);
84
+ all.set(added, before.length);
85
+ await this.#fs.mkdir(dirname(resolved));
86
+ await this.#fs.writeFile(resolved, all);
87
+ });
88
+ }
89
+ truncateFile(path, size, context) {
90
+ const resolved = this.#path(path);
91
+ if (!Number.isSafeInteger(size) || size < 0) {
92
+ return Promise.resolve(
93
+ err(new FileError("invalid", "File size must be a non-negative integer", resolved))
94
+ );
95
+ }
96
+ return attempt(context, resolved, async () => {
97
+ const before = await this.#fs.readFile(resolved);
98
+ const after = new Uint8Array(size);
99
+ after.set(before.subarray(0, size));
100
+ await this.#fs.writeFile(resolved, after);
101
+ });
102
+ }
103
+ flushFile(path, context) {
104
+ const resolved = this.#path(path);
105
+ return attempt(context, resolved, async () => {
106
+ if ((await this.#fs.stat(resolved)).isDirectory)
107
+ throw Object.assign(new Error("Is a directory"), { code: "EISDIR" });
108
+ });
109
+ }
110
+ renameFile(sourcePath, destinationPath, context) {
111
+ const source = this.#path(sourcePath);
112
+ return attempt(context, source, () => this.#fs.rename(source, this.#path(destinationPath)));
113
+ }
114
+ fileInfo(path, context) {
115
+ const resolved = this.#path(path);
116
+ return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));
117
+ }
118
+ async listDir(path, context) {
119
+ const reader = await this.openDirReader(path, context);
120
+ if (!reader.ok) return reader;
121
+ return ok(await reader.value.rest());
122
+ }
123
+ openDirReader(path, context) {
124
+ const resolved = this.#path(path);
125
+ return attempt(context, resolved, async () => {
126
+ if (!(await this.#fs.stat(resolved)).isDirectory) {
127
+ throw Object.assign(new Error("Not a directory"), { code: "ENOTDIR" });
128
+ }
129
+ return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));
130
+ });
131
+ }
132
+ watch(targets, onChange, context) {
133
+ return attempt(context, this.cwd, async () => {
134
+ const watcher = await pollWatch(this.#fs, this.cwd, targets, onChange, this.#poll);
135
+ this.#watchers.add(watcher);
136
+ return {
137
+ mode: watcher.mode,
138
+ close: async (closing) => {
139
+ this.#watchers.delete(watcher);
140
+ await watcher.close(closing);
141
+ }
142
+ };
143
+ });
144
+ }
145
+ canonicalPath(path, context) {
146
+ const resolved = this.#path(path);
147
+ return attempt(context, resolved, () => this.#fs.realpath(resolved));
148
+ }
149
+ async exists(path, context) {
150
+ const found = await this.fileInfo(path, context);
151
+ if (found.ok) return ok(true);
152
+ return found.error.code === "not_found" ? ok(false) : found;
153
+ }
154
+ createDir(path, _options, context) {
155
+ const resolved = this.#path(path);
156
+ return attempt(context, resolved, () => this.#fs.mkdir(resolved));
157
+ }
158
+ remove(path, options, context) {
159
+ const resolved = this.#path(path);
160
+ return attempt(context, resolved, async () => {
161
+ if (!options?.recursive) {
162
+ const stat = await this.#fs.lstat(resolved).catch((error) => {
163
+ if (options?.force) return void 0;
164
+ throw error;
165
+ });
166
+ if (stat?.isDirectory) throw Object.assign(new Error("Is a directory"), { code: "EISDIR" });
167
+ }
168
+ await this.#fs.rm(resolved, { force: options?.force ?? false });
169
+ });
170
+ }
171
+ async createTempDir(prefix, context) {
172
+ const path = `/tmp/${prefix ?? "tmp-"}${crypto.randomUUID()}`;
173
+ return attempt(context, path, async () => {
174
+ await this.#fs.mkdir(path);
175
+ return path;
176
+ });
177
+ }
178
+ async createTempFile(options, context) {
179
+ const dir = await this.createTempDir("tmp-", context);
180
+ if (!dir.ok) return dir;
181
+ const path = `${dir.value}/${options?.prefix ?? ""}${crypto.randomUUID()}${options?.suffix ?? ""}`;
182
+ return attempt(context, path, async () => {
183
+ await this.#fs.writeFile(path, "");
184
+ return path;
185
+ });
186
+ }
187
+ exec(command, options, context) {
188
+ return execute(this.#client, this.cwd, command, options, context);
189
+ }
190
+ async cleanup(context) {
191
+ for (const watcher of [...this.#watchers]) await watcher.close(context);
192
+ this.#watchers.clear();
193
+ }
194
+ }
195
+ const HOME = "/home";
196
+ function kernelEnvironment(client, options = {}) {
197
+ return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });
198
+ }
199
+ export {
200
+ HOME,
201
+ SliccKernelEnv,
202
+ kernelEnvironment
203
+ };
204
+ //# sourceMappingURL=env.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/env.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type BinaryReader,\n type DirReader,\n type ExecutionEnv,\n type ExecutionError,\n err,\n FileError,\n type FileInfo,\n type FileWatcher,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n type TextLineReader,\n type WatchChange,\n type WatchTarget,\n} from '@earendil-works/pi-durable/env';\nimport type { KernelClient } from './client.ts';\nimport { execute } from './exec.ts';\nimport { attempt, info, KernelDirReader, SnapshotLines, SnapshotReader } from './files.ts';\nimport { dirname, normalize, resolve } from './paths.ts';\nimport { type PollOptions, pollWatch } from './watch.ts';\n\nexport interface KernelEnvOptions {\n cwd: string;\n id?: string;\n watch?: PollOptions;\n}\n\ntype FileResult<T> = Promise<Result<T, FileError>>;\n\nexport class SliccKernelEnv implements ExecutionEnv {\n readonly id: string;\n cwd: string;\n readonly #client: KernelClient;\n readonly #watchers = new Set<FileWatcher>();\n readonly #poll: PollOptions;\n\n constructor(client: KernelClient, options: KernelEnvOptions) {\n this.#client = client;\n this.cwd = options.cwd;\n this.id = options.id ?? 'slicc-kernel';\n this.#poll = options.watch ?? {};\n }\n\n #path(path: string): string {\n return resolve(this.cwd, path);\n }\n\n get #fs() {\n return this.#client.fs;\n }\n\n async absolutePath(path: string): FileResult<string> {\n return ok(this.#path(path));\n }\n\n async joinPath(parts: string[]): FileResult<string> {\n return ok(normalize(parts.join('/')));\n }\n\n readTextFile(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readText(resolved));\n }\n\n readBinaryFile(path: string, context: Context): FileResult<Uint8Array> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.readFile(resolved));\n }\n\n async openTextLineReader(path: string, context: Context): FileResult<TextLineReader> {\n const text = await this.readTextFile(path, context);\n return text.ok ? ok(new SnapshotLines(text.value)) : text;\n }\n\n async readTextLines(\n path: string,\n options: { maxLines?: number } | undefined,\n context: Context\n ): FileResult<string[]> {\n if (options?.maxLines !== undefined && options.maxLines <= 0) return ok([]);\n const text = await this.readTextFile(path, context);\n if (!text.ok) return text;\n const reader = new SnapshotLines(text.value);\n const lines: string[] = [];\n for (\n let line = await reader.readLine();\n line.ok && line.value;\n line = await reader.readLine()\n ) {\n if (options?.maxLines !== undefined && lines.length >= options.maxLines) break;\n lines.push(line.value.text);\n }\n return ok(lines);\n }\n\n openBinaryReader(\n path: string,\n options: { noFollow?: boolean } | undefined,\n context: Context\n ): FileResult<BinaryReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (options?.noFollow && (await this.#fs.lstat(resolved)).isSymbolicLink) {\n throw new FileError('invalid', 'Refusing to follow a symbolic link', resolved);\n }\n const stat = await this.#fs.stat(resolved);\n if (stat.isDirectory) throw new FileError('is_directory', 'Is a directory', resolved);\n if (!stat.isFile) throw new FileError('invalid', 'Not a regular file', resolved);\n return new SnapshotReader(await this.#fs.readFile(resolved), info(resolved, stat));\n });\n }\n\n writeFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, content);\n });\n }\n\n appendFile(path: string, content: string | Uint8Array, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n const before = (await this.#fs.exists(resolved))\n ? await this.#fs.readFile(resolved)\n : new Uint8Array();\n const added = typeof content === 'string' ? new TextEncoder().encode(content) : content;\n const all = new Uint8Array(before.length + added.length);\n all.set(before);\n all.set(added, before.length);\n await this.#fs.mkdir(dirname(resolved));\n await this.#fs.writeFile(resolved, all);\n });\n }\n\n truncateFile(path: string, size: number, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n if (!Number.isSafeInteger(size) || size < 0) {\n return Promise.resolve(\n err(new FileError('invalid', 'File size must be a non-negative integer', resolved))\n );\n }\n return attempt(context, resolved, async () => {\n const before = await this.#fs.readFile(resolved);\n const after = new Uint8Array(size);\n after.set(before.subarray(0, size));\n await this.#fs.writeFile(resolved, after);\n });\n }\n\n flushFile(path: string, context: Context): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if ((await this.#fs.stat(resolved)).isDirectory)\n throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n });\n }\n\n renameFile(sourcePath: string, destinationPath: string, context: Context): FileResult<void> {\n const source = this.#path(sourcePath);\n return attempt(context, source, () => this.#fs.rename(source, this.#path(destinationPath)));\n }\n\n fileInfo(path: string, context: Context): FileResult<FileInfo> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => info(resolved, await this.#fs.lstat(resolved)));\n }\n\n async listDir(path: string, context: Context): FileResult<FileInfo[]> {\n const reader = await this.openDirReader(path, context);\n if (!reader.ok) return reader;\n return ok(await (reader.value as KernelDirReader).rest());\n }\n\n openDirReader(path: string, context: Context): FileResult<DirReader> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!(await this.#fs.stat(resolved)).isDirectory) {\n throw Object.assign(new Error('Not a directory'), { code: 'ENOTDIR' });\n }\n return new KernelDirReader(this.#fs, resolved, await this.#fs.readdir(resolved));\n });\n }\n\n watch(\n targets: readonly WatchTarget[],\n onChange: (change: WatchChange) => void,\n context: Context\n ): FileResult<FileWatcher> {\n return attempt(context, this.cwd, async () => {\n const watcher = await pollWatch(this.#fs, this.cwd, targets, onChange, this.#poll);\n this.#watchers.add(watcher);\n return {\n mode: watcher.mode,\n close: async (closing: Context) => {\n this.#watchers.delete(watcher);\n await watcher.close(closing);\n },\n };\n });\n }\n\n canonicalPath(path: string, context: Context): FileResult<string> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.realpath(resolved));\n }\n\n async exists(path: string, context: Context): FileResult<boolean> {\n const found = await this.fileInfo(path, context);\n if (found.ok) return ok(true);\n return found.error.code === 'not_found' ? ok(false) : found;\n }\n\n createDir(\n path: string,\n _options: { recursive?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, () => this.#fs.mkdir(resolved));\n }\n\n remove(\n path: string,\n options: { recursive?: boolean; force?: boolean } | undefined,\n context: Context\n ): FileResult<void> {\n const resolved = this.#path(path);\n return attempt(context, resolved, async () => {\n if (!options?.recursive) {\n const stat = await this.#fs.lstat(resolved).catch((error: unknown) => {\n if (options?.force) return undefined;\n throw error;\n });\n if (stat?.isDirectory) throw Object.assign(new Error('Is a directory'), { code: 'EISDIR' });\n }\n await this.#fs.rm(resolved, { force: options?.force ?? false });\n });\n }\n\n async createTempDir(prefix: string | undefined, context: Context): FileResult<string> {\n const path = `/tmp/${prefix ?? 'tmp-'}${crypto.randomUUID()}`;\n return attempt(context, path, async () => {\n await this.#fs.mkdir(path);\n return path;\n });\n }\n\n async createTempFile(\n options: { prefix?: string; suffix?: string } | undefined,\n context: Context\n ): FileResult<string> {\n const dir = await this.createTempDir('tmp-', context);\n if (!dir.ok) return dir;\n const path = `${dir.value}/${options?.prefix ?? ''}${crypto.randomUUID()}${options?.suffix ?? ''}`;\n return attempt(context, path, async () => {\n await this.#fs.writeFile(path, '');\n return path;\n });\n }\n\n exec(\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n ): Promise<Result<ShellExecResult, ExecutionError>> {\n return execute(this.#client, this.cwd, command, options, context);\n }\n\n async cleanup(context: Context): Promise<void> {\n for (const watcher of [...this.#watchers]) await watcher.close(context);\n this.#watchers.clear();\n }\n}\n\nexport const HOME = '/home';\n\nexport function kernelEnvironment(\n client: KernelClient,\n options: Omit<KernelEnvOptions, 'cwd'> = {}\n): (target: { cwd?: string }) => SliccKernelEnv {\n return (target) => new SliccKernelEnv(client, { ...options, cwd: target.cwd ?? HOME });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,115 @@
1
+ import {
2
+ ExecutionError,
3
+ err,
4
+ ok,
5
+ StreamDecoder
6
+ } from "@earendil-works/pi-durable/env";
7
+ import { resolve } from "./paths.js";
8
+ const SHELL = ["bash", "-c"];
9
+ class Spill {
10
+ #chunks = [];
11
+ #limit;
12
+ #bytes = 0;
13
+ #lines = 0;
14
+ constructor(limit) {
15
+ this.#limit = limit;
16
+ }
17
+ add(chunk) {
18
+ if (!this.#limit) return;
19
+ this.#chunks.push(chunk);
20
+ this.#bytes += chunk.length;
21
+ for (const byte of chunk) if (byte === 10) this.#lines++;
22
+ }
23
+ get over() {
24
+ const limit = this.#limit;
25
+ return limit !== void 0 && (this.#bytes > limit.afterBytes || this.#lines > limit.afterLines);
26
+ }
27
+ async write(client) {
28
+ if (!this.over) return void 0;
29
+ const path = `/tmp/slicc-agent-output-${crypto.randomUUID()}.log`;
30
+ const all = new Uint8Array(this.#bytes);
31
+ let offset = 0;
32
+ for (const chunk of this.#chunks) {
33
+ all.set(chunk, offset);
34
+ offset += chunk.length;
35
+ }
36
+ await client.fs.mkdir("/tmp");
37
+ await client.fs.writeFile(path, all);
38
+ return path;
39
+ }
40
+ }
41
+ function sink(stream, spill, options, context) {
42
+ const decoder = new StreamDecoder();
43
+ return {
44
+ push(bytes) {
45
+ spill.add(bytes);
46
+ const text = decoder.decode(bytes);
47
+ if (text) options?.onOutput?.(text, context, { stream });
48
+ },
49
+ end() {
50
+ const text = decoder.decode();
51
+ if (text) options?.onOutput?.(text, context, { stream });
52
+ }
53
+ };
54
+ }
55
+ function guard(process, options, context) {
56
+ let ending;
57
+ const stop = (why) => {
58
+ ending ??= why;
59
+ process.signal("SIGKILL");
60
+ };
61
+ const timer = options?.timeout === void 0 ? void 0 : setTimeout(() => stop("timeout"), options.timeout * 1e3);
62
+ const onAbort = () => stop("aborted");
63
+ context.abortSignal?.addEventListener("abort", onAbort, { once: true });
64
+ if (context.abortSignal?.aborted) onAbort();
65
+ return {
66
+ get ending() {
67
+ return ending;
68
+ },
69
+ release() {
70
+ clearTimeout(timer);
71
+ context.abortSignal?.removeEventListener("abort", onAbort);
72
+ }
73
+ };
74
+ }
75
+ async function execute(client, cwd, command, options, context) {
76
+ const argv = typeof command === "string" ? [...SHELL, command] : [...command];
77
+ if (argv.length === 0) return err(new ExecutionError("spawn_error", "No program to run"));
78
+ if (context.abortSignal?.aborted)
79
+ return err(new ExecutionError("aborted", "The command was aborted"));
80
+ const spill = new Spill(options?.spill);
81
+ const out = sink("stdout", spill, options, context);
82
+ const errors = sink("stderr", spill, options, context);
83
+ let process;
84
+ try {
85
+ process = await client.spawn(argv, {
86
+ cwd: resolve(cwd, options?.cwd ?? "."),
87
+ ...options?.env ? { env: options.env } : {},
88
+ onStdout: (bytes) => out.push(bytes),
89
+ onStderr: (bytes) => errors.push(bytes)
90
+ });
91
+ } catch (error) {
92
+ const message = error instanceof Error ? error.message : String(error);
93
+ return err(new ExecutionError("spawn_error", message));
94
+ }
95
+ const watch = guard(process, options, context);
96
+ const exitCode = await process.exited.finally(() => watch.release());
97
+ out.end();
98
+ errors.end();
99
+ const spillPath = await spill.write(client);
100
+ const ending = watch.ending;
101
+ if (ending) {
102
+ const error = new ExecutionError(
103
+ ending,
104
+ `The command was ${ending === "timeout" ? "timed out" : "aborted"}`
105
+ );
106
+ if (spillPath) error.spillPath = spillPath;
107
+ return err(error);
108
+ }
109
+ return ok(spillPath ? { exitCode, spillPath } : { exitCode });
110
+ }
111
+ export {
112
+ SHELL,
113
+ execute
114
+ };
115
+ //# sourceMappingURL=exec.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/kernel/exec.ts"],
4
+ "sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n ExecutionError,\n err,\n ok,\n type Result,\n type ShellExecOptions,\n type ShellExecResult,\n StreamDecoder,\n} from '@earendil-works/pi-durable/env';\nimport type { KernelClient, KernelProcess } from './client.ts';\nimport { resolve } from './paths.ts';\n\nexport const SHELL = ['bash', '-c'] as const;\n\nclass Spill {\n readonly #chunks: Uint8Array[] = [];\n readonly #limit: { afterBytes: number; afterLines: number } | undefined;\n #bytes = 0;\n #lines = 0;\n\n constructor(limit: ShellExecOptions['spill']) {\n this.#limit = limit;\n }\n\n add(chunk: Uint8Array): void {\n if (!this.#limit) return;\n this.#chunks.push(chunk);\n this.#bytes += chunk.length;\n for (const byte of chunk) if (byte === 10) this.#lines++;\n }\n\n get over(): boolean {\n const limit = this.#limit;\n return (\n limit !== undefined && (this.#bytes > limit.afterBytes || this.#lines > limit.afterLines)\n );\n }\n\n async write(client: KernelClient): Promise<string | undefined> {\n if (!this.over) return undefined;\n const path = `/tmp/slicc-agent-output-${crypto.randomUUID()}.log`;\n const all = new Uint8Array(this.#bytes);\n let offset = 0;\n for (const chunk of this.#chunks) {\n all.set(chunk, offset);\n offset += chunk.length;\n }\n await client.fs.mkdir('/tmp');\n await client.fs.writeFile(path, all);\n return path;\n }\n}\n\nfunction sink(\n stream: 'stdout' | 'stderr',\n spill: Spill,\n options: ShellExecOptions | undefined,\n context: Context\n) {\n const decoder = new StreamDecoder();\n return {\n push(bytes: Uint8Array) {\n spill.add(bytes);\n const text = decoder.decode(bytes);\n if (text) options?.onOutput?.(text, context, { stream });\n },\n end() {\n const text = decoder.decode();\n if (text) options?.onOutput?.(text, context, { stream });\n },\n };\n}\n\ntype Ending = 'timeout' | 'aborted' | undefined;\n\nfunction guard(process: KernelProcess, options: ShellExecOptions | undefined, context: Context) {\n let ending: Ending;\n const stop = (why: Ending) => {\n ending ??= why;\n process.signal('SIGKILL');\n };\n const timer =\n options?.timeout === undefined\n ? undefined\n : setTimeout(() => stop('timeout'), options.timeout * 1000);\n const onAbort = () => stop('aborted');\n context.abortSignal?.addEventListener('abort', onAbort, { once: true });\n if (context.abortSignal?.aborted) onAbort();\n return {\n get ending() {\n return ending;\n },\n release() {\n clearTimeout(timer);\n context.abortSignal?.removeEventListener('abort', onAbort);\n },\n };\n}\n\nexport async function execute(\n client: KernelClient,\n cwd: string,\n command: string | readonly string[],\n options: ShellExecOptions | undefined,\n context: Context\n): Promise<Result<ShellExecResult, ExecutionError>> {\n const argv = typeof command === 'string' ? [...SHELL, command] : [...command];\n if (argv.length === 0) return err(new ExecutionError('spawn_error', 'No program to run'));\n if (context.abortSignal?.aborted)\n return err(new ExecutionError('aborted', 'The command was aborted'));\n const spill = new Spill(options?.spill);\n const out = sink('stdout', spill, options, context);\n const errors = sink('stderr', spill, options, context);\n let process: KernelProcess;\n try {\n process = await client.spawn(argv, {\n cwd: resolve(cwd, options?.cwd ?? '.'),\n ...(options?.env ? { env: options.env } : {}),\n onStdout: (bytes) => out.push(bytes),\n onStderr: (bytes) => errors.push(bytes),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return err(new ExecutionError('spawn_error', message));\n }\n const watch = guard(process, options, context);\n const exitCode = await process.exited.finally(() => watch.release());\n out.end();\n errors.end();\n const spillPath = await spill.write(client);\n const ending = watch.ending;\n if (ending) {\n const error = new ExecutionError(\n ending,\n `The command was ${ending === 'timeout' ? 'timed out' : 'aborted'}`\n );\n if (spillPath) error.spillPath = spillPath;\n return err(error);\n }\n return ok(spillPath ? { exitCode, spillPath } : { exitCode });\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }