@omac/pi 0.0.1-beta.2 → 0.0.1-beta.4

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 (2) hide show
  1. package/package.json +5 -5
  2. package/src/extension.ts +35 -37
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@omac/pi",
3
- "version": "0.0.1-beta.2",
4
- "description": "omac's Pi integration: the Worker's turn-end report, the Coordinator's inbox delivery, and both roles' guard.",
3
+ "version": "0.0.1-beta.4",
4
+ "description": "omac's Pi integration: a session's turn-end report, the coordinator's inbox delivery, and both agents' guard.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -26,9 +26,9 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "@effect/platform-node-shared": "4.0.2",
29
- "effect-herdr-sdk": "0.0.1-beta.2",
29
+ "effect-herdr-sdk": "0.0.1-beta.4",
30
30
  "effect": "4.0.2",
31
- "@omac/cli": "0.0.1-beta.2"
31
+ "@omac/cli": "0.0.1-beta.4"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@earendil-works/pi-coding-agent": "1.1.0"
@@ -38,6 +38,6 @@
38
38
  },
39
39
  "scripts": {
40
40
  "typecheck": "tsc --noEmit",
41
- "test": "cd ../.. && bun --bun vitest run packages/pi-omac"
41
+ "test": "cd ../.. && bun ./node_modules/vitest/vitest.mjs run packages/pi-omac"
42
42
  }
43
43
  }
package/src/extension.ts CHANGED
@@ -1,13 +1,13 @@
1
- // omac's Pi extension. OMAC_WORKER_ID makes it a Worker (turn-end result file), OMAC_COORDINATOR=1 a Coordinator (inbox
2
- // rows delivered in process instead of typed into the pane); otherwise it is inert. Either way tool calls go through
3
- // that role's rules in omac's guard.ts, in process. Both roles run omac itself as `omac` commands in Pi's bash tool.
1
+ // omac's Pi extension. OMAC_AGENT=coordinator makes it the coordinator (inbox rows delivered in process instead of typed
2
+ // into the pane), any other agent with OMAC_SESSION_ID a session (turn-end result file); otherwise it is inert. Either way
3
+ // tool calls go through that agent's rules in omac's guard.ts, in process. Both run omac itself as `omac` commands in Pi's bash tool.
4
4
  import type { AgentEndEvent, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"
5
5
  import * as NodeChildProcessSpawner from "@effect/platform-node-shared/NodeChildProcessSpawner"
6
6
  import * as NodeFileSystem from "@effect/platform-node-shared/NodeFileSystem"
7
7
  import * as NodePath from "@effect/platform-node-shared/NodePath"
8
8
  import * as NodeSocket from "@effect/platform-node-shared/NodeSocket"
9
9
  import { SocketConnector } from "effect-herdr-sdk"
10
- import { type ClientServices, connect, withOmac } from "@omac/cli/src/client.ts"
10
+ import { type ClientServices, connect, protocolHeaders, withOmac } from "@omac/cli/src/client.ts"
11
11
  import * as Guard from "@omac/cli/src/guard.ts"
12
12
  import { Layout, OmacCommand, writeAtomic } from "@omac/cli/src/paths.ts"
13
13
  import { Cause, Clock, Config, Deferred, Effect, FileSystem, Layer, Option, Path, Ref, Schedule, Schema, Stream } from "effect"
@@ -15,7 +15,7 @@ import { Pi } from "./pi.ts"
15
15
 
16
16
  const encodeJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown))
17
17
 
18
- /** omac's tool-call rules for this role, as Pi's tool_call result. */
18
+ /** omac's tool-call rules for this agent, as Pi's tool_call result. */
19
19
  const guarded = (rules: Guard.Rules) =>
20
20
  Effect.gen(function* () {
21
21
  const pi = yield* Pi
@@ -34,16 +34,15 @@ const lastReply = (messages: ReadonlyArray<AgentMessage>) => {
34
34
  return { text, error: message?.stopReason === "error" ? message.errorMessage || "error" : "" }
35
35
  }
36
36
 
37
- /** What a role's layer needs; the roles share one type, so picking one is a plain conditional. */
37
+ /** What an agent's layer needs; they share one type, so picking one is a plain conditional. */
38
38
  type Role = Layer.Layer<never, Config.ConfigError, Pi | ClientServices | FileSystem.FileSystem>
39
39
 
40
- const worker = (workerId: string): Role =>
40
+ const session = (sessionId: string): Role =>
41
41
  Layer.effectDiscard(
42
42
  Effect.gen(function* () {
43
43
  const pi = yield* Pi
44
44
  const layout = yield* Layout
45
- yield* guarded(Guard.worker)
46
- // TUI only: RPC / JSON / print modes have no PTY, so they are not the Worker's root session.
45
+ // TUI only: RPC / JSON / print modes have no PTY, so they are not the session's root (harness) session.
47
46
  const root = yield* Ref.make(false)
48
47
  const reply = yield* Ref.make({ text: "", error: "" })
49
48
  yield* pi.handle("session_start", (_e, ctx) => Ref.set(root, ctx.mode === "tui"))
@@ -54,10 +53,10 @@ const worker = (workerId: string): Role =>
54
53
  const { text, error } = yield* Ref.get(reply)
55
54
  const ts = Math.floor((yield* Clock.currentTimeMillis) / 1000)
56
55
  const payload = { text, error: error || (e.aborted ? "aborted" : ""), aborted: e.aborted }
57
- yield* writeAtomic(layout.resultFile(workerId), encodeJson({ harness: "pi", ts, payload }))
56
+ yield* writeAtomic(layout.resultFile(sessionId), encodeJson({ harness: "pi", ts, payload }))
58
57
  // omacd also finds the file on its next check; the call only makes that immediate.
59
58
  yield* connect(layout.socket).pipe(
60
- Effect.flatMap((client) => client.ResultReady({ workerId })),
59
+ Effect.flatMap((client) => client.ResultReady({ sessionId }, { headers: protocolHeaders() })),
61
60
  Effect.scoped,
62
61
  Effect.timeout("500 millis"),
63
62
  Effect.ignore,
@@ -68,31 +67,28 @@ const worker = (workerId: string): Role =>
68
67
  )
69
68
 
70
69
  /**
71
- * `omac launch pi` reopens the session it reported: after a TUI turn (Pi has written the session by `agent_end`) whose
72
- * session id is not reported yet, send it in the background, so neither the turn nor omacd's absence holds Pi up. omacd
73
- * keeps the newest by `reportedAt`, whatever order sends arrive in; one omacd did not answer warns and is sent again after
74
- * the next turn. Only from the process `omac launch` exec'd: a Pi nested in the Coordinator inherits OMAC_LAUNCH_ID.
70
+ * `omac launch pi` reopens the conversation it reported: after a TUI turn (Pi has written the session by `agent_end`)
71
+ * whose id is not reported yet, send it in the background. The last report wins. A Pi child in print / RPC mode does
72
+ * not report (this runs only for the TUI).
75
73
  */
76
- const reportSession = Effect.fn("reportSession")(function* (launchId: string) {
74
+ const reportSession = Effect.fn("reportSession")(function* (sessionId: string) {
77
75
  const pi = yield* Pi
78
76
  const layout = yield* Layout
79
- if ((yield* Config.String("OMAC_LAUNCH_PID").pipe(Config.withDefault(""))) !== String(process.pid)) return
80
77
  const scope = yield* Effect.scope
81
78
  const reported = yield* Ref.make("")
82
79
  yield* pi.handle("agent_end", (_e, ctx) =>
83
80
  Effect.gen(function* () {
84
- const sessionId = ctx.sessionManager.getSessionId()
85
- if (ctx.mode !== "tui" || (yield* Ref.get(reported)) === sessionId) return
86
- const reportedAt = yield* Clock.currentTimeMillis
87
- // Never starts omacd: `omac launch` already did, and the launch is in omac.db for whichever omacd answers.
81
+ const harnessSessionId = ctx.sessionManager.getSessionId()
82
+ if (ctx.mode !== "tui" || (yield* Ref.get(reported)) === harnessSessionId) return
83
+ // Never starts omacd: `omac launch` already did.
88
84
  yield* connect(layout.socket).pipe(
89
- Effect.flatMap((client) => client.CoordinatorSessionReady({ launchId, harness: "pi", sessionId, reportedAt })),
85
+ Effect.flatMap((client) => client.CoordinatorSessionReady({ sessionId, harness: "pi", harnessSessionId }, { headers: protocolHeaders() })),
90
86
  Effect.scoped,
91
87
  Effect.timeout("1 second"),
92
- Effect.andThen(Ref.set(reported, sessionId)),
88
+ Effect.andThen(Ref.set(reported, harnessSessionId)),
93
89
  // Unanswered is not unsaved: omacd may still apply it (and keeps the newest either way).
94
90
  Effect.catchCause((cause) =>
95
- Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.sync(() => ctx.ui.notify(`omac: the Coordinator session may not have been saved: ${Cause.pretty(cause)}`, "warning")),
91
+ Cause.hasInterruptsOnly(cause) ? Effect.void : Effect.sync(() => ctx.ui.notify(`omac: the coordinator's conversation may not have been saved: ${Cause.pretty(cause)}`, "warning")),
96
92
  ),
97
93
  Effect.forkIn(scope),
98
94
  )
@@ -104,20 +100,17 @@ const reportSession = Effect.fn("reportSession")(function* (launchId: string) {
104
100
  const coordinator: Role = Layer.effectDiscard(
105
101
  Effect.gen(function* () {
106
102
  const pi = yield* Pi
107
- yield* guarded(Guard.coordinator)
108
103
  // Clear the prompt sections other extensions add for a coding agent. Pi runs these handlers in extension load order,
109
104
  // so only sections added by extensions loaded before pi-omac are cleared; ones loaded after it keep theirs.
110
105
  // Turns started by inbox rows skip this event and get the base prompt, which never has them.
111
106
  yield* pi.handle("before_agent_start", (e) => Effect.sync(() => { e.systemPromptOptions.sections = {} }))
112
- const launchId = yield* Config.option(Config.String("OMAC_LAUNCH_ID"))
113
- if (Option.isSome(launchId)) yield* reportSession(launchId.value)
114
- const paneId = yield* Config.option(Config.String("HERDR_PANE_ID"))
115
- // Outside a herdr pane there is no inbox to subscribe to.
116
- if (Option.isNone(paneId)) return
117
- // Pi children of a Coordinator (print / RPC mode) inherit OMAC_COORDINATOR; only the TUI session takes the rows.
107
+ const sessionId = yield* Config.option(Config.String("OMAC_SESSION_ID"))
108
+ if (Option.isNone(sessionId)) return
109
+ yield* reportSession(sessionId.value)
110
+ // Pi children of the coordinator (print / RPC mode) inherit OMAC_AGENT; only the TUI session takes the rows.
118
111
  const started = yield* Deferred.make<ExtensionContext>()
119
112
  yield* pi.handle("session_start", (_e, ctx) => (ctx.mode === "tui" ? Deferred.succeed(started, ctx).pipe(Effect.asVoid) : Effect.void))
120
- const subscribe = withOmac({ paneId: paneId.value }, (client) =>
113
+ const subscribe = withOmac({ _tag: "session", sessionId: sessionId.value }, (client) =>
121
114
  client["Inbox.Subscribe"](undefined).pipe(
122
115
  Stream.runForEach((content) => pi.sendMessage({ customType: "omac", content, display: false }, { triggerTurn: true, deliverAs: "followUp" })),
123
116
  ),
@@ -140,13 +133,18 @@ const coordinator: Role = Layer.effectDiscard(
140
133
  }),
141
134
  )
142
135
 
143
- /** The role from the environment: Worker, Coordinator, or nothing at all. */
136
+ /**
137
+ * The agent from the environment: OMAC_AGENT alone decides the tool rules, as in the hook script; the coordinator also
138
+ * takes its inbox rows, another agent with OMAC_SESSION_ID writes its turn's result. No OMAC_AGENT: nothing at all.
139
+ */
144
140
  export const extension = Layer.unwrap(
145
141
  Effect.gen(function* () {
146
- const workerId = Option.getOrUndefined(yield* Config.option(Config.String("OMAC_WORKER_ID")))
147
- const isCoordinator = Option.contains(yield* Config.option(Config.String("OMAC_COORDINATOR")), "1")
148
- if (!workerId && !isCoordinator) return Layer.empty
149
- return workerId ? worker(workerId) : coordinator
142
+ const agent = Option.getOrUndefined(yield* Config.option(Config.String("OMAC_AGENT")))
143
+ if (!agent) return Layer.empty
144
+ const guard: Role = Layer.effectDiscard(guarded(Guard.rulesOf(agent)))
145
+ if (agent === Guard.COORDINATOR) return coordinator.pipe(Layer.provideMerge(guard))
146
+ const sessionId = Option.getOrUndefined(yield* Config.option(Config.String("OMAC_SESSION_ID")))
147
+ return sessionId ? session(sessionId).pipe(Layer.provideMerge(guard)) : guard
150
148
  }),
151
149
  )
152
150