@jpowersdev/effect-pi 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/CHANGELOG.md +15 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/ClusterSessions.d.ts +2 -0
- package/dist/ClusterSessions.d.ts.map +1 -0
- package/dist/ClusterSessions.js +2 -0
- package/dist/ClusterSessions.js.map +1 -0
- package/dist/LocalSessions.d.ts +2 -0
- package/dist/LocalSessions.d.ts.map +1 -0
- package/dist/LocalSessions.js +2 -0
- package/dist/LocalSessions.js.map +1 -0
- package/dist/ModelRuntime.d.ts +2 -0
- package/dist/ModelRuntime.d.ts.map +1 -0
- package/dist/ModelRuntime.js +2 -0
- package/dist/ModelRuntime.js.map +1 -0
- package/dist/ResourceLoader.d.ts +2 -0
- package/dist/ResourceLoader.d.ts.map +1 -0
- package/dist/ResourceLoader.js +2 -0
- package/dist/ResourceLoader.js.map +1 -0
- package/dist/Session.d.ts +3 -0
- package/dist/Session.d.ts.map +1 -0
- package/dist/Session.js +3 -0
- package/dist/Session.js.map +1 -0
- package/dist/Sessions.d.ts +14 -0
- package/dist/Sessions.d.ts.map +1 -0
- package/dist/Sessions.js +5 -0
- package/dist/Sessions.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/ClusterSessions.d.ts +44 -0
- package/dist/internal/ClusterSessions.d.ts.map +1 -0
- package/dist/internal/ClusterSessions.js +72 -0
- package/dist/internal/ClusterSessions.js.map +1 -0
- package/dist/internal/LocalSessions.d.ts +19 -0
- package/dist/internal/LocalSessions.d.ts.map +1 -0
- package/dist/internal/LocalSessions.js +25 -0
- package/dist/internal/LocalSessions.js.map +1 -0
- package/dist/internal/ModelRuntime.d.ts +42 -0
- package/dist/internal/ModelRuntime.d.ts.map +1 -0
- package/dist/internal/ModelRuntime.js +66 -0
- package/dist/internal/ModelRuntime.js.map +1 -0
- package/dist/internal/Persistence.d.ts +12 -0
- package/dist/internal/Persistence.d.ts.map +1 -0
- package/dist/internal/Persistence.js +26 -0
- package/dist/internal/Persistence.js.map +1 -0
- package/dist/internal/Pi.d.ts +51 -0
- package/dist/internal/Pi.d.ts.map +1 -0
- package/dist/internal/Pi.js +258 -0
- package/dist/internal/Pi.js.map +1 -0
- package/dist/internal/Protocol.d.ts +41 -0
- package/dist/internal/Protocol.d.ts.map +1 -0
- package/dist/internal/Protocol.js +33 -0
- package/dist/internal/Protocol.js.map +1 -0
- package/dist/internal/ResourceLoader.d.ts +46 -0
- package/dist/internal/ResourceLoader.d.ts.map +1 -0
- package/dist/internal/ResourceLoader.js +77 -0
- package/dist/internal/ResourceLoader.js.map +1 -0
- package/dist/internal/Session.d.ts +82 -0
- package/dist/internal/Session.d.ts.map +1 -0
- package/dist/internal/Session.js +63 -0
- package/dist/internal/Session.js.map +1 -0
- package/dist-examples/cluster-session.d.ts +2 -0
- package/dist-examples/cluster-session.d.ts.map +1 -0
- package/dist-examples/cluster-session.js +77 -0
- package/dist-examples/cluster-session.js.map +1 -0
- package/dist-examples/local-session-pool.d.ts +2 -0
- package/dist-examples/local-session-pool.d.ts.map +1 -0
- package/dist-examples/local-session-pool.js +60 -0
- package/dist-examples/local-session-pool.js.map +1 -0
- package/dist-examples/single-session.d.ts +2 -0
- package/dist-examples/single-session.d.ts.map +1 -0
- package/dist-examples/single-session.js +49 -0
- package/dist-examples/single-session.js.map +1 -0
- package/docs/reference.md +248 -0
- package/examples/README.md +111 -0
- package/examples/cluster-session.ts +131 -0
- package/examples/local-session-pool.ts +107 -0
- package/examples/single-session.ts +85 -0
- package/package.json +62 -0
- package/src/ClusterSessions.ts +6 -0
- package/src/LocalSessions.ts +1 -0
- package/src/ModelRuntime.ts +10 -0
- package/src/ResourceLoader.ts +13 -0
- package/src/Session.ts +14 -0
- package/src/Sessions.ts +17 -0
- package/src/index.ts +6 -0
- package/src/internal/ClusterSessions.ts +126 -0
- package/src/internal/LocalSessions.ts +45 -0
- package/src/internal/ModelRuntime.ts +105 -0
- package/src/internal/Persistence.ts +40 -0
- package/src/internal/Pi.ts +346 -0
- package/src/internal/Protocol.ts +39 -0
- package/src/internal/ResourceLoader.ts +128 -0
- package/src/internal/Session.ts +109 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Create one session, send a prompt, and save its history in SQLite. */
|
|
2
|
+
import * as NodeRuntime from "@effect/platform-node/NodeRuntime"
|
|
3
|
+
import * as NodeServices from "@effect/platform-node/NodeServices"
|
|
4
|
+
import * as SqliteClient from "@effect/sql-sqlite-node/SqliteClient"
|
|
5
|
+
|
|
6
|
+
import * as Config from "effect/Config"
|
|
7
|
+
import * as Console from "effect/Console"
|
|
8
|
+
import * as Effect from "effect/Effect"
|
|
9
|
+
import * as Layer from "effect/Layer"
|
|
10
|
+
import * as Stream from "effect/Stream"
|
|
11
|
+
import * as Argument from "effect/unstable/cli/Argument"
|
|
12
|
+
import * as Command from "effect/unstable/cli/Command"
|
|
13
|
+
import * as Flag from "effect/unstable/cli/Flag"
|
|
14
|
+
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
|
|
15
|
+
|
|
16
|
+
import { ModelRuntime, ResourceLoader, Session } from "@jpowersdev/effect-pi"
|
|
17
|
+
|
|
18
|
+
const ResourcesLive = ResourceLoader.layerEmpty({
|
|
19
|
+
systemPrompt: "You are a helpful assistant. Use the read-only tools only when asked to inspect files.",
|
|
20
|
+
settings: { retry: { enabled: false } }
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
const ModelLive = ModelRuntime.layerConfig(
|
|
24
|
+
Config.all({
|
|
25
|
+
provider: Config.NonEmptyString("EFFECT_PI_PROVIDER"),
|
|
26
|
+
modelId: Config.NonEmptyString("EFFECT_PI_MODEL"),
|
|
27
|
+
apiKey: Config.Redacted("EFFECT_PI_API_KEY")
|
|
28
|
+
}).pipe(
|
|
29
|
+
Config.map(({ provider, modelId, apiKey }) => ({
|
|
30
|
+
model: { provider, id: modelId },
|
|
31
|
+
apiKeys: { [provider]: apiKey },
|
|
32
|
+
authPath: ".data/effect-pi/auth.json",
|
|
33
|
+
modelsPath: null,
|
|
34
|
+
refreshOnCreate: false
|
|
35
|
+
}))
|
|
36
|
+
)
|
|
37
|
+
).pipe(
|
|
38
|
+
Layer.provide(ResourcesLive)
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
const StoreLive = KeyValueStore.layerSql({ table: "pi_sessions" }).pipe(
|
|
42
|
+
Layer.provide(
|
|
43
|
+
SqliteClient.layer({ filename: ".data/effect-pi/sessions.sqlite" })
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
const cli = Command.make("single-session", {
|
|
48
|
+
prompt: Argument.String("prompt").pipe(
|
|
49
|
+
Argument.withDefault("Say hello in one short sentence.")
|
|
50
|
+
),
|
|
51
|
+
snapshot: Flag.Boolean("snapshot").pipe(
|
|
52
|
+
Flag.withDefault(false),
|
|
53
|
+
Flag.withDescription("Print saved state without calling a model")
|
|
54
|
+
)
|
|
55
|
+
}, ({ prompt, snapshot }) =>
|
|
56
|
+
Effect.gen(function* () {
|
|
57
|
+
const session = yield* Session.make({
|
|
58
|
+
id: Session.Id.make("single-session"),
|
|
59
|
+
cwd: ".",
|
|
60
|
+
configure: () => ({ tools: ["read", "grep", "find", "ls"] })
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (snapshot) {
|
|
64
|
+
return yield* Console.log(yield* session.snapshot)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
yield* session.events.pipe(
|
|
68
|
+
Stream.runForEach((event) => Console.log(event)),
|
|
69
|
+
Effect.catch((error) => Console.warn(error)),
|
|
70
|
+
Effect.forkScoped({ startImmediately: true })
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const result = yield* session.prompt(prompt).pipe(Effect.timeout("2 minutes"))
|
|
74
|
+
|
|
75
|
+
yield* Console.log("Result:", result)
|
|
76
|
+
}).pipe(Effect.scoped)
|
|
77
|
+
).pipe(
|
|
78
|
+
Command.provide(Layer.merge(ModelLive, StoreLive))
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const program = Command.run(cli, { version: "0.1.0" }).pipe(
|
|
82
|
+
Effect.provide(NodeServices.layer)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
NodeRuntime.runMain(program)
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jpowersdev/effect-pi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local and clustered Effect services for durable Pi coding-agent sessions",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/jpowersdev/effect-pi.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/jpowersdev/effect-pi#readme",
|
|
11
|
+
"bugs": "https://github.com/jpowersdev/effect-pi/issues",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"src/**/*.ts",
|
|
22
|
+
"examples/*.ts",
|
|
23
|
+
"examples/README.md",
|
|
24
|
+
"dist-examples",
|
|
25
|
+
"docs/**/*.md",
|
|
26
|
+
"CHANGELOG.md",
|
|
27
|
+
"CONTRIBUTING.md"
|
|
28
|
+
],
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"import": "./dist/index.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=26.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@earendil-works/pi-coding-agent": "0.86.1"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"effect": "4.0.0-rc.116"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@effect/platform-node": "4.0.0-rc.116",
|
|
46
|
+
"@effect/sql-sqlite-node": "4.0.0-rc.116",
|
|
47
|
+
"effect": "4.0.0-rc.116",
|
|
48
|
+
"@effect/vitest": "4.0.0-rc.116",
|
|
49
|
+
"@types/node": "26.4.1",
|
|
50
|
+
"typescript": "7.0.2",
|
|
51
|
+
"vitest": "5.0.1"
|
|
52
|
+
},
|
|
53
|
+
"scripts": {
|
|
54
|
+
"build": "tsc -b src examples",
|
|
55
|
+
"check": "tsc -b",
|
|
56
|
+
"clean": "tsc -b --clean && rm -rf dist dist-test dist-examples .tsbuildinfo",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"example:session": "pnpm build && node dist-examples/single-session.js",
|
|
59
|
+
"example:pool": "pnpm build && node dist-examples/local-session-pool.js",
|
|
60
|
+
"example:cluster": "pnpm build && node dist-examples/cluster-session.js"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { layer, layerConfig, type Options } from "./internal/LocalSessions.js"
|
package/src/Session.ts
ADDED
package/src/Sessions.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as Context from "effect/Context"
|
|
2
|
+
import type * as Effect from "effect/Effect"
|
|
3
|
+
import type * as Scope from "effect/Scope"
|
|
4
|
+
|
|
5
|
+
import type * as Session from "./internal/Session.js"
|
|
6
|
+
|
|
7
|
+
export interface Operations {
|
|
8
|
+
/** Acquire the session for an id within the current scope. */
|
|
9
|
+
readonly open: (
|
|
10
|
+
id: Session.Id
|
|
11
|
+
) => Effect.Effect<Session.Session, Session.Error, Scope.Scope>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Location-independent access to keyed Pi sessions. */
|
|
15
|
+
export class Sessions extends Context.Service<Sessions, Operations>()(
|
|
16
|
+
"@jpowersdev/effect-pi/Sessions"
|
|
17
|
+
) {}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * as ClusterSessions from "./ClusterSessions.js"
|
|
2
|
+
export * as LocalSessions from "./LocalSessions.js"
|
|
3
|
+
export * as ModelRuntime from "./ModelRuntime.js"
|
|
4
|
+
export * as ResourceLoader from "./ResourceLoader.js"
|
|
5
|
+
export * as Session from "./Session.js"
|
|
6
|
+
export { Sessions } from "./Sessions.js"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import * as Config from "effect/Config"
|
|
2
|
+
import type * as Duration from "effect/Duration"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Layer from "effect/Layer"
|
|
5
|
+
import * as Schema from "effect/Schema"
|
|
6
|
+
import type * as Scope from "effect/Scope"
|
|
7
|
+
import * as Stream from "effect/Stream"
|
|
8
|
+
import * as Entity from "effect/unstable/cluster/Entity"
|
|
9
|
+
import * as EntityResource from "effect/unstable/cluster/EntityResource"
|
|
10
|
+
import type * as Sharding from "effect/unstable/cluster/Sharding"
|
|
11
|
+
|
|
12
|
+
import { Sessions } from "../Sessions.js"
|
|
13
|
+
import * as Pi from "./Pi.js"
|
|
14
|
+
import * as Protocol from "./Protocol.js"
|
|
15
|
+
import * as Session from "./Session.js"
|
|
16
|
+
|
|
17
|
+
export interface Options extends Session.Config {
|
|
18
|
+
/** Idle live-resource lifetime; defaults to 15 minutes. */
|
|
19
|
+
readonly resourceIdleTimeToLive?: Duration.Input
|
|
20
|
+
readonly entityMaxIdleTime?: Duration.Input
|
|
21
|
+
/** Queued entity messages; does not limit all concurrently executing handlers. Defaults to 128. */
|
|
22
|
+
readonly mailboxCapacity?: number | "unbounded"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type MakeSession<R> = (
|
|
26
|
+
options: Session.MakeOptions
|
|
27
|
+
) => Effect.Effect<Session.Session, Session.Error, Scope.Scope | R>
|
|
28
|
+
|
|
29
|
+
const failureMessage = (cause: unknown): string => {
|
|
30
|
+
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
31
|
+
return String(cause.message)
|
|
32
|
+
}
|
|
33
|
+
return String(cause)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const transportError = (
|
|
37
|
+
id: Session.Id,
|
|
38
|
+
operation: Session.Operation,
|
|
39
|
+
cause: unknown
|
|
40
|
+
) => cause instanceof Session.Error
|
|
41
|
+
? cause
|
|
42
|
+
: new Session.Error({
|
|
43
|
+
sessionId: id,
|
|
44
|
+
operation,
|
|
45
|
+
message: failureMessage(cause)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
/** Internal constructor seam used to test entity behavior without model calls. */
|
|
49
|
+
export const runnerLayerWith = <R>(
|
|
50
|
+
options: Options,
|
|
51
|
+
makeSession: MakeSession<R>
|
|
52
|
+
): Layer.Layer<never, never, Sharding.Sharding | R> => {
|
|
53
|
+
const handlers = Effect.gen(function*() {
|
|
54
|
+
const address = yield* Entity.CurrentAddress
|
|
55
|
+
const id = yield* Schema.decodeUnknownEffect(Session.Id)(address.entityId).pipe(
|
|
56
|
+
Effect.orDie
|
|
57
|
+
)
|
|
58
|
+
const resource = yield* EntityResource.make({
|
|
59
|
+
acquire: makeSession({
|
|
60
|
+
id,
|
|
61
|
+
cwd: options.cwd,
|
|
62
|
+
...(options.keyPrefix === undefined ? {} : { keyPrefix: options.keyPrefix }),
|
|
63
|
+
...(options.configure === undefined ? {} : { configure: options.configure })
|
|
64
|
+
}),
|
|
65
|
+
idleTimeToLive: options.resourceIdleTimeToLive ?? "15 minutes"
|
|
66
|
+
}).pipe(Effect.orDie)
|
|
67
|
+
|
|
68
|
+
return Protocol.entity.of({
|
|
69
|
+
Snapshot: () => resource.get.pipe(Effect.flatMap((session) => session.snapshot)),
|
|
70
|
+
Prompt: (request) => resource.get.pipe(
|
|
71
|
+
Effect.flatMap((session) => session.prompt(request.payload.text))
|
|
72
|
+
),
|
|
73
|
+
Abort: () => resource.get.pipe(Effect.flatMap((session) => session.abort)),
|
|
74
|
+
Jsonl: () => resource.get.pipe(Effect.flatMap((session) => session.jsonl)),
|
|
75
|
+
Events: () => Stream.unwrap(resource.get.pipe(
|
|
76
|
+
Effect.map((session) => session.events)
|
|
77
|
+
))
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
return Protocol.entity.toLayer(handlers, {
|
|
82
|
+
concurrency: "unbounded",
|
|
83
|
+
mailboxCapacity: options.mailboxCapacity ?? 128,
|
|
84
|
+
maxIdleTime: options.entityMaxIdleTime ?? "20 minutes"
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Registers the server-side cluster entity handlers. */
|
|
89
|
+
export const runnerLayer = (options: Options) => runnerLayerWith(options, Pi.make)
|
|
90
|
+
|
|
91
|
+
/** Resolve runner options using the active ConfigProvider at layer build time. */
|
|
92
|
+
export const runnerLayerConfig = (options: Config.Wrap<Options>) =>
|
|
93
|
+
Layer.unwrap(Effect.map(Config.unwrap<Options>(options), runnerLayer))
|
|
94
|
+
|
|
95
|
+
/** Implements Sessions using location-transparent Effect Cluster clients. */
|
|
96
|
+
export const clientLayer: Layer.Layer<Sessions, never, Sharding.Sharding> = Layer.effect(
|
|
97
|
+
Sessions,
|
|
98
|
+
Protocol.entity.client.pipe(
|
|
99
|
+
Effect.map((makeClient) => Sessions.of({
|
|
100
|
+
open: (id) => Effect.succeed((() => {
|
|
101
|
+
const client = makeClient(id)
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
snapshot: client.Snapshot().pipe(
|
|
105
|
+
Effect.mapError((cause) => transportError(id, "snapshot", cause))
|
|
106
|
+
),
|
|
107
|
+
prompt: (text) => Schema.decodeUnknownEffect(Schema.NonEmptyString)(text).pipe(
|
|
108
|
+
Effect.flatMap((text) => client.Prompt({ text })),
|
|
109
|
+
Effect.mapError((cause) => transportError(id, "prompt", cause))
|
|
110
|
+
),
|
|
111
|
+
abort: client.Abort().pipe(
|
|
112
|
+
Effect.mapError((cause) => transportError(id, "abort", cause))
|
|
113
|
+
),
|
|
114
|
+
events: client.Events().pipe(
|
|
115
|
+
Stream.mapError((cause) => transportError(id, "events", cause))
|
|
116
|
+
),
|
|
117
|
+
jsonl: client.Jsonl().pipe(
|
|
118
|
+
Effect.mapError((cause) => transportError(id, "jsonl", cause))
|
|
119
|
+
)
|
|
120
|
+
} satisfies Session.Session
|
|
121
|
+
})())
|
|
122
|
+
}))
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
export const entity = Protocol.entity
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as Config from "effect/Config"
|
|
2
|
+
import type * as Duration from "effect/Duration"
|
|
3
|
+
import * as Effect from "effect/Effect"
|
|
4
|
+
import * as Layer from "effect/Layer"
|
|
5
|
+
import * as RcMap from "effect/RcMap"
|
|
6
|
+
import type * as Scope from "effect/Scope"
|
|
7
|
+
|
|
8
|
+
import { Sessions } from "../Sessions.js"
|
|
9
|
+
import * as Pi from "./Pi.js"
|
|
10
|
+
import type * as Session from "./Session.js"
|
|
11
|
+
|
|
12
|
+
export interface Options extends Session.Config {
|
|
13
|
+
/** How long an unreferenced live Pi session remains resident. */
|
|
14
|
+
readonly idleTimeToLive?: Duration.Input
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type MakeSession<R> = (
|
|
18
|
+
options: Session.MakeOptions
|
|
19
|
+
) => Effect.Effect<Session.Session, Session.Error, Scope.Scope | R>
|
|
20
|
+
|
|
21
|
+
/** Internal constructor seam used to verify pool behavior without model calls. */
|
|
22
|
+
export const layerWith = <R>(
|
|
23
|
+
options: Options,
|
|
24
|
+
makeSession: MakeSession<R>
|
|
25
|
+
): Layer.Layer<Sessions, never, R> => Layer.effect(Sessions, Effect.gen(function*() {
|
|
26
|
+
const sessions = yield* RcMap.make({
|
|
27
|
+
lookup: (id: Session.Id) => makeSession({
|
|
28
|
+
id,
|
|
29
|
+
cwd: options.cwd,
|
|
30
|
+
...(options.keyPrefix === undefined ? {} : { keyPrefix: options.keyPrefix }),
|
|
31
|
+
...(options.configure === undefined ? {} : { configure: options.configure })
|
|
32
|
+
}),
|
|
33
|
+
idleTimeToLive: options.idleTimeToLive ?? "15 minutes"
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return Sessions.of({
|
|
37
|
+
open: (id) => RcMap.get(sessions, id)
|
|
38
|
+
})
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
export const layer = (options: Options) => layerWith(options, Pi.make)
|
|
42
|
+
|
|
43
|
+
/** Resolve pool options using the active ConfigProvider at layer build time. */
|
|
44
|
+
export const layerConfig = (options: Config.Wrap<Options>) =>
|
|
45
|
+
Layer.unwrap(Effect.map(Config.unwrap<Options>(options), layer))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import * as Pi from "@earendil-works/pi-coding-agent"
|
|
2
|
+
import * as Config from "effect/Config"
|
|
3
|
+
import * as Context from "effect/Context"
|
|
4
|
+
import * as Effect from "effect/Effect"
|
|
5
|
+
import * as Layer from "effect/Layer"
|
|
6
|
+
import * as Redacted from "effect/Redacted"
|
|
7
|
+
import * as Schema from "effect/Schema"
|
|
8
|
+
import type * as Scope from "effect/Scope"
|
|
9
|
+
|
|
10
|
+
import * as ResourceLoader from "./ResourceLoader.js"
|
|
11
|
+
|
|
12
|
+
export interface Options extends Omit<Pi.CreateModelRuntimeOptions, "signal"> {
|
|
13
|
+
/** Omit to let Pi restore/select the model using the session and settings. */
|
|
14
|
+
readonly model?: { readonly provider: string; readonly id: string }
|
|
15
|
+
|
|
16
|
+
/** Runtime-only overrides. Keys are never persisted to auth.json. */
|
|
17
|
+
readonly apiKeys?: Readonly<Record<string, Redacted.Redacted<string>>>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class Error extends Schema.TaggedError<Error>()("ModelRuntimeError", {
|
|
21
|
+
operation: Schema.Literals(["create", "providers", "credentials", "model"]),
|
|
22
|
+
message: Schema.String
|
|
23
|
+
}) {}
|
|
24
|
+
|
|
25
|
+
/** SDK interoperability values; never share one binding between live sessions. */
|
|
26
|
+
export interface Binding extends ResourceLoader.Loaded {
|
|
27
|
+
readonly modelRuntime: Pi.ModelRuntime
|
|
28
|
+
readonly model?: NonNullable<Pi.CreateAgentSessionOptions["model"]>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface Operations {
|
|
32
|
+
readonly sessionOptions: (
|
|
33
|
+
cwd: string
|
|
34
|
+
) => Effect.Effect<Binding, Error | ResourceLoader.Error, Scope.Scope>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ModelRuntime extends Context.Service<ModelRuntime, Operations>()(
|
|
38
|
+
"@jpowersdev/effect-pi/ModelRuntime"
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
/** Capture the loading policy; SDK objects are owned by each live session's scope. */
|
|
42
|
+
export const make = Effect.fn("ModelRuntime.make")(function* (options: Options = {}) {
|
|
43
|
+
const resources = yield* ResourceLoader.ResourceLoader
|
|
44
|
+
|
|
45
|
+
const { model: selection, apiKeys, ...runtimeOptions } = options
|
|
46
|
+
|
|
47
|
+
const sessionOptions: Operations["sessionOptions"] = Effect.fn("ModelRuntime.sessionOptions")(function* (cwd) {
|
|
48
|
+
const loaded = yield* resources.load(cwd)
|
|
49
|
+
|
|
50
|
+
const modelRuntime = yield* Effect.tryPromise({
|
|
51
|
+
try: (signal) => Pi.ModelRuntime.create({ ...runtimeOptions, signal }),
|
|
52
|
+
// SDK credential errors can contain raw keys. Do not retain or stringify them.
|
|
53
|
+
catch: () => new Error({ operation: "create", message: "Unable to create Pi model runtime" })
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// Extensions can define the selected model. Register their providers before
|
|
57
|
+
// lookup; leave Pi's pending queues intact for its own subsequent binding.
|
|
58
|
+
yield* Effect.try({
|
|
59
|
+
try: () => {
|
|
60
|
+
const runtime = loaded.resourceLoader.getExtensions().runtime
|
|
61
|
+
|
|
62
|
+
for (const { name, config } of runtime.pendingProviderRegistrations) {
|
|
63
|
+
modelRuntime.registerProvider(name, config)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const { provider } of runtime.pendingNativeProviderRegistrations) {
|
|
67
|
+
modelRuntime.registerNativeProvider(provider)
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
catch: () => new Error({ operation: "providers", message: "Unable to register resource providers" })
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
for (const [provider, key] of Object.entries(apiKeys ?? {})) {
|
|
74
|
+
yield* Effect.tryPromise({
|
|
75
|
+
try: (signal) => modelRuntime.setRuntimeApiKey(provider, Redacted.value(key), { signal }),
|
|
76
|
+
catch: () => new Error({ operation: "credentials", message: "Unable to configure runtime credentials" })
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (selection === undefined) return { ...loaded, modelRuntime }
|
|
81
|
+
|
|
82
|
+
const model = yield* Effect.try({
|
|
83
|
+
try: () => modelRuntime.getModel(selection.provider, selection.id),
|
|
84
|
+
catch: () => new Error({ operation: "model", message: "Unable to resolve the configured model" })
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (model === undefined) {
|
|
88
|
+
return yield* new Error({
|
|
89
|
+
operation: "model",
|
|
90
|
+
message: `Unknown model ${selection.provider}/${selection.id}`
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { ...loaded, modelRuntime, model }
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
return ModelRuntime.of({ sessionOptions })
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
export const layer = (options: Options = {}): Layer.Layer<ModelRuntime, never, ResourceLoader.ResourceLoader> =>
|
|
101
|
+
Layer.effect(ModelRuntime, make(options))
|
|
102
|
+
|
|
103
|
+
/** Resolve configuration when the layer is built, using the active ConfigProvider. */
|
|
104
|
+
export const layerConfig = (options: Config.Wrap<Options>) =>
|
|
105
|
+
Layer.unwrap(Effect.map(Config.unwrap<Options>(options), layer))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect"
|
|
2
|
+
import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"
|
|
3
|
+
|
|
4
|
+
import * as Session from "./Session.js"
|
|
5
|
+
|
|
6
|
+
const message = (cause: unknown): string => {
|
|
7
|
+
if (typeof cause === "object" && cause !== null && "message" in cause) {
|
|
8
|
+
return String(cause.message)
|
|
9
|
+
}
|
|
10
|
+
return String(cause)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface Store {
|
|
14
|
+
readonly load: Effect.Effect<string | undefined, Session.Error>
|
|
15
|
+
readonly save: (jsonl: string) => Effect.Effect<void, Session.Error>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const make = Effect.fnUntraced(function* (
|
|
19
|
+
id: Session.Id,
|
|
20
|
+
keyPrefix = "effect-pi/sessions/"
|
|
21
|
+
) {
|
|
22
|
+
const backing = yield* KeyValueStore.KeyValueStore
|
|
23
|
+
const store = KeyValueStore.prefix(backing, keyPrefix)
|
|
24
|
+
return {
|
|
25
|
+
load: store.get(id).pipe(
|
|
26
|
+
Effect.mapError((cause) => new Session.Error({
|
|
27
|
+
sessionId: id,
|
|
28
|
+
operation: "load",
|
|
29
|
+
message: message(cause)
|
|
30
|
+
}))
|
|
31
|
+
),
|
|
32
|
+
save: (jsonl) => store.set(id, jsonl).pipe(
|
|
33
|
+
Effect.mapError((cause) => new Session.Error({
|
|
34
|
+
sessionId: id,
|
|
35
|
+
operation: "save",
|
|
36
|
+
message: message(cause)
|
|
37
|
+
}))
|
|
38
|
+
)
|
|
39
|
+
} satisfies Store
|
|
40
|
+
})
|