@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.
- package/package.json +5 -5
- 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.
|
|
4
|
-
"description": "omac's Pi integration:
|
|
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.
|
|
29
|
+
"effect-herdr-sdk": "0.0.1-beta.4",
|
|
30
30
|
"effect": "4.0.2",
|
|
31
|
-
"@omac/cli": "0.0.1-beta.
|
|
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
|
|
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.
|
|
2
|
-
//
|
|
3
|
-
// that
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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({
|
|
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
|
|
72
|
-
*
|
|
73
|
-
*
|
|
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* (
|
|
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
|
|
85
|
-
if (ctx.mode !== "tui" || (yield* Ref.get(reported)) ===
|
|
86
|
-
|
|
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({
|
|
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,
|
|
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
|
|
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
|
|
113
|
-
if (Option.
|
|
114
|
-
|
|
115
|
-
//
|
|
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({
|
|
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
|
-
/**
|
|
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
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|