@shivaedev/agent-pi 0.1.0 → 0.3.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 (63) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +55 -1
  3. package/dist/PiOptions.d.ts +5 -0
  4. package/dist/PiOptions.d.ts.map +1 -0
  5. package/dist/PiOptions.js +2 -0
  6. package/dist/PiOptions.js.map +1 -0
  7. package/dist/adapters/deliver.d.ts +4 -0
  8. package/dist/adapters/deliver.d.ts.map +1 -0
  9. package/dist/adapters/deliver.js +19 -0
  10. package/dist/adapters/deliver.js.map +1 -0
  11. package/dist/adapters/effort.d.ts +5 -0
  12. package/dist/adapters/effort.d.ts.map +1 -0
  13. package/dist/adapters/effort.js +13 -0
  14. package/dist/adapters/effort.js.map +1 -0
  15. package/dist/adapters/failure.d.ts +6 -0
  16. package/dist/adapters/failure.d.ts.map +1 -0
  17. package/dist/adapters/failure.js +14 -0
  18. package/dist/adapters/failure.js.map +1 -0
  19. package/dist/adapters/listPiModels.d.ts +8 -0
  20. package/dist/adapters/listPiModels.d.ts.map +1 -0
  21. package/dist/adapters/listPiModels.js +11 -0
  22. package/dist/adapters/listPiModels.js.map +1 -0
  23. package/dist/adapters/mapping.d.ts +9 -0
  24. package/dist/adapters/mapping.d.ts.map +1 -0
  25. package/dist/adapters/mapping.js +44 -0
  26. package/dist/adapters/mapping.js.map +1 -0
  27. package/dist/adapters/messageEvents.d.ts +9 -0
  28. package/dist/adapters/messageEvents.d.ts.map +1 -0
  29. package/dist/adapters/messageEvents.js +41 -0
  30. package/dist/adapters/messageEvents.js.map +1 -0
  31. package/dist/adapters/openSession.d.ts +323 -0
  32. package/dist/adapters/openSession.d.ts.map +1 -0
  33. package/dist/adapters/openSession.js +41 -0
  34. package/dist/adapters/openSession.js.map +1 -0
  35. package/dist/adapters/runtime.d.ts +24 -0
  36. package/dist/adapters/runtime.d.ts.map +1 -0
  37. package/dist/adapters/runtime.js +60 -0
  38. package/dist/adapters/runtime.js.map +1 -0
  39. package/dist/adapters/textOf.d.ts +3 -0
  40. package/dist/adapters/textOf.d.ts.map +1 -0
  41. package/dist/adapters/textOf.js +4 -0
  42. package/dist/adapters/textOf.js.map +1 -0
  43. package/dist/makeOperations.d.ts +668 -0
  44. package/dist/makeOperations.d.ts.map +1 -0
  45. package/dist/makeOperations.js +55 -0
  46. package/dist/makeOperations.js.map +1 -0
  47. package/dist/pi.d.ts +4 -1
  48. package/dist/pi.d.ts.map +1 -1
  49. package/dist/pi.js +39 -1
  50. package/dist/pi.js.map +1 -1
  51. package/package.json +29 -6
  52. package/src/PiOptions.ts +4 -0
  53. package/src/adapters/deliver.ts +24 -0
  54. package/src/adapters/effort.ts +15 -0
  55. package/src/adapters/failure.ts +14 -0
  56. package/src/adapters/listPiModels.ts +16 -0
  57. package/src/adapters/mapping.ts +50 -0
  58. package/src/adapters/messageEvents.ts +50 -0
  59. package/src/adapters/openSession.ts +46 -0
  60. package/src/adapters/runtime.ts +70 -0
  61. package/src/adapters/textOf.ts +5 -0
  62. package/src/makeOperations.ts +75 -0
  63. package/src/pi.ts +44 -1
@@ -0,0 +1,55 @@
1
+ import { Effect, Exit, Scope, Stream } from "effect";
2
+ import { UnknownAcceptance } from "@shivaedev/agent/failures.ts";
3
+ import { protocolFailure } from "#adapters/failure.ts";
4
+ import { openSession } from "#adapters/openSession.ts";
5
+ import { textOf } from "#adapters/textOf.ts";
6
+ export const makeOperations = Effect.fnUntraced(function* (options) {
7
+ const runs = yield* Effect.sync(() => new Map());
8
+ const evidence = new Map();
9
+ function lookup(receipt) {
10
+ return Effect.suspend(() => {
11
+ const run = runs.get(receipt.operationId);
12
+ return receipt.backend === "pi" && receipt.reference.version === 1 && run?.receipt.reference.value === receipt.reference.value
13
+ ? Effect.succeed(run)
14
+ : Effect.fail(protocolFailure("run is not open in this backend scope"));
15
+ });
16
+ }
17
+ const submit = Effect.fnUntraced(function* (request, resume) {
18
+ const run = yield* openSession(options, request, resume);
19
+ yield* run.prompt(textOf(request.input), "steer").pipe(Effect.mapError((error) => {
20
+ if (error._tag === "Rejected") {
21
+ evidence.set(request.operationId, { evidence: error.message, operationId: request.operationId, type: "not-accepted" });
22
+ return error;
23
+ }
24
+ return new UnknownAcceptance({ backend: "pi", message: error.message, operationId: request.operationId });
25
+ }));
26
+ runs.set(request.operationId, run);
27
+ evidence.set(request.operationId, { receipt: run.receipt, type: "accepted" });
28
+ yield* run.acknowledge;
29
+ yield* Effect.addFinalizer(() => Effect.sync(() => {
30
+ runs.delete(request.operationId);
31
+ }));
32
+ return run.receipt;
33
+ });
34
+ const open = Effect.fnUntraced(function* (request, resume) {
35
+ if (evidence.has(request.operationId)) {
36
+ return yield* Effect.fail(protocolFailure("operation already attempted; reconcile instead"));
37
+ }
38
+ evidence.set(request.operationId, { operationId: request.operationId, reason: "submission has no acceptance evidence yet", type: "uncertain" });
39
+ const runScope = yield* Scope.fork(yield* Effect.scope);
40
+ return yield* submit(request, resume).pipe(Effect.provideService(Scope.Scope, runScope), Effect.onError((cause) => Scope.close(runScope, Exit.failCause(cause))));
41
+ });
42
+ return {
43
+ events: (receipt) => Stream.unwrap(lookup(receipt).pipe(Effect.map((run) => run.events))),
44
+ inspect: (receipt) => lookup(receipt).pipe(Effect.flatMap((run) => run.inspect), Effect.catchTag("ProtocolFailure", () => Effect.succeed({ reason: "no live execution evidence in this backend scope", type: "missing" }))),
45
+ lookup,
46
+ open,
47
+ reconcile: (operationId) => Effect.sync(() => evidence.get(operationId) ?? {
48
+ operationId,
49
+ reason: "Pi has no operation-id lookup; missing local evidence does not prove non-acceptance",
50
+ type: "uncertain",
51
+ }),
52
+ resume: (reference, request) => reference.version === 1 ? open(request, reference.value) : Effect.fail(protocolFailure("unsupported session reference version")),
53
+ };
54
+ });
55
+ //# sourceMappingURL=makeOperations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"makeOperations.js","sourceRoot":"","sources":["../src/makeOperations.ts"],"names":[],"mappings":"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"}
package/dist/pi.d.ts CHANGED
@@ -1,2 +1,5 @@
1
- export {};
1
+ import { Layer } from "effect";
2
+ import { Backend } from "@shivaedev/agent/backend.ts";
3
+ import type { PiOptions } from "#PiOptions.ts";
4
+ export declare function pi(options?: PiOptions): Layer.Layer<Backend>;
2
5
  //# sourceMappingURL=pi.d.ts.map
package/dist/pi.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../src/pi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"pi.d.ts","sourceRoot":"","sources":["../src/pi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,OAAO,EAA0B,MAAM,6BAA6B,CAAC;AAI9E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE/C,wBAAgB,EAAE,CAAC,OAAO,GAAE,SAAc,GAAG,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC,CAoChE"}
package/dist/pi.js CHANGED
@@ -1,2 +1,40 @@
1
- export {};
1
+ import { Effect, Layer } from "effect";
2
+ import { Backend } from "@shivaedev/agent/backend.ts";
3
+ import { listPiModels } from "#adapters/listPiModels.ts";
4
+ import { textOf } from "#adapters/textOf.ts";
5
+ import { makeOperations } from "#makeOperations.ts";
6
+ export function pi(options = {}) {
7
+ return Layer.effect(Backend, Effect.gen(function* () {
8
+ const operations = yield* makeOperations(options);
9
+ return {
10
+ backend: "pi",
11
+ events: operations.events,
12
+ features: {
13
+ imageInput: { reason: "only text input is implemented", support: "unsupported" },
14
+ interruption: {
15
+ implementation: { interrupt: (receipt) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.interrupt)) },
16
+ support: "supported",
17
+ },
18
+ models: { implementation: { list: listPiModels }, support: "supported" },
19
+ queuedInput: {
20
+ implementation: {
21
+ queue: (receipt, input) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.prompt(textOf(input), "followUp"))),
22
+ },
23
+ support: "supported",
24
+ },
25
+ sessions: { implementation: { resume: operations.resume }, support: "supported" },
26
+ steering: {
27
+ implementation: {
28
+ steer: (receipt, input) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.prompt(textOf(input), "steer"))),
29
+ },
30
+ support: "supported",
31
+ },
32
+ transcript: { reason: "no provider history reader is implemented", support: "unsupported" },
33
+ },
34
+ inspect: operations.inspect,
35
+ reconcile: operations.reconcile,
36
+ start: (request) => operations.open(request, undefined),
37
+ };
38
+ }));
39
+ }
2
40
  //# sourceMappingURL=pi.js.map
package/dist/pi.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"pi.js","sourceRoot":"","sources":["../src/pi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"pi.js","sourceRoot":"","sources":["../src/pi.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,OAAO,EAA0B,MAAM,6BAA6B,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAGpD,MAAM,UAAU,EAAE,CAAC,OAAO,GAAc,EAAE;IACzC,OAAO,KAAK,CAAC,MAAM,CAClB,OAAO,EACP,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QACnB,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC;QAClD,OAAO;YACN,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,UAAU,CAAC,MAAM;YACzB,QAAQ,EAAE;gBACT,UAAU,EAAE,EAAE,MAAM,EAAE,gCAAgC,EAAE,OAAO,EAAE,aAAa,EAAE;gBAChF,YAAY,EAAE;oBACb,cAAc,EAAE,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,EAAE;oBACnH,OAAO,EAAE,WAAW;iBACpB;gBACD,MAAM,EAAE,EAAE,cAAc,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE;gBACxE,WAAW,EAAE;oBACZ,cAAc,EAAE;wBACf,KAAK,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;qBAC1H;oBACD,OAAO,EAAE,WAAW;iBACpB;gBACD,QAAQ,EAAE,EAAE,cAAc,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE;gBACjF,QAAQ,EAAE;oBACT,cAAc,EAAE;wBACf,KAAK,EAAE,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;qBACvH;oBACD,OAAO,EAAE,WAAW;iBACpB;gBACD,UAAU,EAAE,EAAE,MAAM,EAAE,2CAA2C,EAAE,OAAO,EAAE,aAAa,EAAE;aAC3F;YACD,OAAO,EAAE,UAAU,CAAC,OAAO;YAC3B,SAAS,EAAE,UAAU,CAAC,SAAS;YAC/B,KAAK,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,OAAO,EAAE,SAAS,CAAC;SACvD,CAAC;IACH,CAAC,CAAC,CACF,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/agent-pi",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pi integration for Effect applications",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/agent-pi#readme",
6
6
  "bugs": {
@@ -16,14 +16,15 @@
16
16
  "type": "module",
17
17
  "imports": {
18
18
  "#*.ts": {
19
- "source": "./src/*.ts",
19
+ "@shivaedev/source": "./src/*.ts",
20
20
  "types": "./dist/*.d.ts",
21
21
  "default": "./dist/*.js"
22
- }
22
+ },
23
+ "#test/*": "./src/test-support/*"
23
24
  },
24
25
  "exports": {
25
26
  "./*.ts": {
26
- "source": "./src/*.ts",
27
+ "@shivaedev/source": "./src/*.ts",
27
28
  "types": "./dist/*.d.ts",
28
29
  "default": "./dist/*.js"
29
30
  },
@@ -32,12 +33,33 @@
32
33
  "files": [
33
34
  "dist",
34
35
  "src",
36
+ "!src/**/*.test.ts",
37
+ "!src/**/*.spec.ts",
38
+ "!src/**/test-support",
35
39
  "CHANGELOG.md",
36
40
  "README.md"
37
41
  ],
42
+ "dependencies": {
43
+ "@shivaedev/agent": "0.3.0"
44
+ },
38
45
  "devDependencies": {
46
+ "@earendil-works/pi-coding-agent": "0.84.4",
47
+ "@effect/vitest": "4.0.6",
48
+ "@shivaedev/quality": "0.61.0",
49
+ "@shivaedev/test-story": "0.39.0",
39
50
  "@types/node": "24.19.2",
40
- "typescript": "7.0.2"
51
+ "effect": "4.0.6",
52
+ "typescript": "7.0.2",
53
+ "vitest": "5.0.5"
54
+ },
55
+ "peerDependencies": {
56
+ "@earendil-works/pi-coding-agent": "0.84.4",
57
+ "effect": "4.0.6"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "@earendil-works/pi-coding-agent": {
61
+ "optional": true
62
+ }
41
63
  },
42
64
  "engines": {
43
65
  "node": ">=24"
@@ -48,7 +70,8 @@
48
70
  },
49
71
  "scripts": {
50
72
  "build": "node --eval \"import('node:fs').then(({ rmSync }) => rmSync('dist', { force: true, recursive: true }))\" && tsc --project tsconfig.build.json",
51
- "ready": "pnpm typecheck && pnpm build",
73
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
74
+ "test": "vitest run --configLoader native",
52
75
  "typecheck": "tsc --noEmit"
53
76
  }
54
77
  }
@@ -0,0 +1,4 @@
1
+ export interface PiOptions {
2
+ readonly constrainedPrompt?: string;
3
+ readonly skills?: readonly string[];
4
+ }
@@ -0,0 +1,24 @@
1
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
2
+ import { Effect } from "effect";
3
+ import type { BackendFailure } from "@shivaedev/agent/failures.ts";
4
+ import { protocolFailure, rejected } from "#adapters/failure.ts";
5
+
6
+ export function deliver(session: AgentSession, text: string, delivery: "followUp" | "steer") {
7
+ return Effect.callback<void, BackendFailure>((resume) => {
8
+ let preflight: boolean | undefined;
9
+ session
10
+ .prompt(text, {
11
+ preflightResult: (ok) => {
12
+ preflight = ok;
13
+ if (ok) {
14
+ resume(Effect.void);
15
+ }
16
+ },
17
+ streamingBehavior: delivery,
18
+ })
19
+ .then(
20
+ () => resume(Effect.void),
21
+ (error: unknown) => resume(Effect.fail(preflight === false ? rejected(error) : protocolFailure(error))),
22
+ );
23
+ });
24
+ }
@@ -0,0 +1,15 @@
1
+ import { Effect } from "effect";
2
+ import { rejected } from "#adapters/failure.ts";
3
+
4
+ export const piEfforts = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
5
+ export type PiThinkingLevel = (typeof piEfforts)[number];
6
+
7
+ export function thinkingLevel(effort: string | undefined) {
8
+ if (effort === undefined) {
9
+ return Effect.succeed(undefined);
10
+ }
11
+ const level = piEfforts.find((candidate) => candidate === effort);
12
+ return level === undefined
13
+ ? Effect.fail(rejected(`effort ${effort} is not a thinking level Pi offers; choose one of ${piEfforts.join(", ")}`))
14
+ : Effect.succeed(level);
15
+ }
@@ -0,0 +1,14 @@
1
+ import { BackendUnavailable, ProtocolFailure, Rejected } from "@shivaedev/agent/failures.ts";
2
+
3
+ export function describe(detail: unknown): string {
4
+ return detail instanceof Error ? detail.message : String(detail);
5
+ }
6
+ export function unavailable(detail: unknown) {
7
+ return new BackendUnavailable({ backend: "pi", message: describe(detail) });
8
+ }
9
+ export function protocolFailure(detail: unknown) {
10
+ return new ProtocolFailure({ backend: "pi", message: describe(detail) });
11
+ }
12
+ export function rejected(detail: unknown) {
13
+ return new Rejected({ backend: "pi", message: describe(detail) });
14
+ }
@@ -0,0 +1,16 @@
1
+ import { Effect } from "effect";
2
+ import { piEfforts } from "#adapters/effort.ts";
3
+ import { unavailable } from "#adapters/failure.ts";
4
+ import { loadSdk } from "#adapters/runtime.ts";
5
+
6
+ export const listPiModels = loadSdk.pipe(
7
+ Effect.flatMap((sdk) => Effect.tryPromise({ catch: unavailable, try: async () => (await sdk.ModelRuntime.create()).getAvailable() })),
8
+ Effect.map((models) =>
9
+ models.map((model, index) => ({
10
+ efforts: piEfforts,
11
+ id: `${model.provider}/${model.id}`,
12
+ isDefault: index === 0,
13
+ name: `${model.name} (${model.provider})`,
14
+ })),
15
+ ),
16
+ );
@@ -0,0 +1,50 @@
1
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
2
+ import { Option, Schema } from "effect";
3
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
4
+ import { messageEvents } from "./messageEvents.ts";
5
+
6
+ export function rawOf(kind: string, payload: unknown) {
7
+ return { kind, payload: JSON.stringify(payload), source: "pi" };
8
+ }
9
+ const decodeOutput = Schema.decodeUnknownOption(
10
+ Schema.Struct({ content: Schema.Array(Schema.Struct({ text: Schema.optional(Schema.String), type: Schema.String })) }),
11
+ );
12
+
13
+ export function toAgentEvents(event: AgentSessionEvent): RunEvent[] {
14
+ const raw = rawOf(event.type, event);
15
+ switch (event.type) {
16
+ case "agent_start":
17
+ return [{ raw, state: "running", type: "execution" }];
18
+ case "agent_end": {
19
+ if (event.willRetry) {
20
+ return [{ raw, type: "raw" }];
21
+ }
22
+ const ending = event.messages.filter((message) => message.role === "assistant").at(-1);
23
+ const state = endState(ending?.stopReason);
24
+ return [{ raw, state, type: "execution" }];
25
+ }
26
+ case "message_end":
27
+ return messageEvents(event.message, raw);
28
+ case "tool_execution_start":
29
+ return [{ input: JSON.stringify(event.args), name: event.toolName, raw, toolId: event.toolCallId, type: "tool.started" }];
30
+ case "tool_execution_end": {
31
+ const output = Option.match(decodeOutput(event.result), {
32
+ onNone: () => "",
33
+ onSome: ({ content }) => content.flatMap((part) => (part.type === "text" && part.text !== undefined ? [part.text] : [])).join("\n"),
34
+ });
35
+ return [{ ok: !event.isError, output, raw, toolId: event.toolCallId, type: "tool.completed" }];
36
+ }
37
+ default:
38
+ return [{ raw, type: "raw" }];
39
+ }
40
+ }
41
+
42
+ function endState(reason: string | undefined) {
43
+ if (reason === "aborted") {
44
+ return "interrupted";
45
+ }
46
+ if (reason === "error") {
47
+ return "failed";
48
+ }
49
+ return "completed";
50
+ }
@@ -0,0 +1,50 @@
1
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
2
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
3
+
4
+ type PiMessage = Extract<AgentSessionEvent, { type: "message_end" }>["message"];
5
+ type Raw = NonNullable<RunEvent["raw"]>;
6
+
7
+ export function messageEvents(message: PiMessage, raw: Raw): RunEvent[] {
8
+ if (message.role === "user") {
9
+ const text =
10
+ typeof message.content === "string" ? message.content : message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n");
11
+ return [{ parts: [{ text, type: "text" }], raw, role: "user", type: "message" }];
12
+ }
13
+ if (message.role !== "assistant") {
14
+ return [{ raw, type: "raw" }];
15
+ }
16
+ const events: RunEvent[] = [];
17
+ const thinking = message.content.flatMap((part) => (part.type === "thinking" ? [part.thinking] : [])).join("\n");
18
+ const text = message.content.flatMap((part) => (part.type === "text" ? [part.text] : [])).join("\n");
19
+ if (thinking !== "") {
20
+ events.push({ raw, text: thinking, type: "thinking" });
21
+ }
22
+ if (text !== "") {
23
+ events.push({ parts: [{ text, type: "text" }], raw, role: "agent", type: "message" });
24
+ }
25
+ const model = `${message.provider}/${message.model}`;
26
+ events.push(
27
+ {
28
+ cacheReadTokens: message.usage.cacheRead,
29
+ cacheWriteTokens: message.usage.cacheWrite,
30
+ inputTokens: message.usage.input,
31
+ model,
32
+ outputTokens: message.usage.output,
33
+ raw,
34
+ scope: "delta",
35
+ type: "usage",
36
+ unit: "turn",
37
+ },
38
+ {
39
+ amount: message.usage.cost.total,
40
+ currency: "USD",
41
+ model,
42
+ raw,
43
+ scope: "delta",
44
+ source: "provider",
45
+ type: "cost",
46
+ unit: "turn",
47
+ },
48
+ );
49
+ return events;
50
+ }
@@ -0,0 +1,46 @@
1
+ import { type Cause, Effect, Queue, Stream } from "effect";
2
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
3
+ import type { Observation, Receipt, StartRequest } from "@shivaedev/agent/run.ts";
4
+ import { deliver } from "#adapters/deliver.ts";
5
+ import { unavailable } from "#adapters/failure.ts";
6
+ import { rawOf, toAgentEvents } from "#adapters/mapping.ts";
7
+ import { openRuntime } from "#adapters/runtime.ts";
8
+ import type { PiOptions } from "#PiOptions.ts";
9
+
10
+ export const openSession = Effect.fnUntraced(function* (options: PiOptions, request: StartRequest, resume: string | undefined) {
11
+ const { session, sessionFile } = yield* openRuntime(options, request, resume);
12
+ const output = yield* Queue.unbounded<RunEvent, Cause.Done>();
13
+ let observation: Observation = { state: "accepted", type: "known" };
14
+ function emit(event: RunEvent) {
15
+ if (event.type === "execution") {
16
+ observation = { state: event.state, type: "known" };
17
+ }
18
+ Queue.offerUnsafe(output, event);
19
+ }
20
+ const unsubscribe = session.subscribe((event) => {
21
+ for (const mapped of toAgentEvents(event)) {
22
+ emit(mapped);
23
+ }
24
+ });
25
+ yield* Effect.addFinalizer(() =>
26
+ Effect.sync(() => {
27
+ unsubscribe();
28
+ Queue.endUnsafe(output);
29
+ }),
30
+ );
31
+ const receipt: Receipt = {
32
+ backend: "pi",
33
+ operationId: request.operationId,
34
+ reference: { value: sessionFile, version: 1 },
35
+ session: { value: sessionFile, version: 1 },
36
+ };
37
+ emit({ raw: rawOf("session/opened", { sessionFile, sessionId: session.sessionId }), reference: sessionFile, type: "session.opened" });
38
+ return {
39
+ acknowledge: Effect.sync(() => emit({ receipt, type: "acknowledged" })),
40
+ events: Stream.fromQueue(output),
41
+ inspect: Effect.sync(() => observation),
42
+ interrupt: Effect.tryPromise({ catch: unavailable, try: () => session.abort() }),
43
+ prompt: (text: string, delivery: "followUp" | "steer") => deliver(session, text, delivery),
44
+ receipt,
45
+ };
46
+ });
@@ -0,0 +1,70 @@
1
+ import { access } from "node:fs/promises";
2
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
3
+ import { Effect } from "effect";
4
+ import type { StartRequest } from "@shivaedev/agent/run.ts";
5
+ import { thinkingLevel } from "#adapters/effort.ts";
6
+ import { rejected, unavailable } from "#adapters/failure.ts";
7
+ import type { PiOptions } from "#PiOptions.ts";
8
+
9
+ export const loadSdk = Effect.tryPromise({ catch: unavailable, try: () => import("@earendil-works/pi-coding-agent") });
10
+
11
+ export const openRuntime = Effect.fnUntraced(function* (options: PiOptions, request: StartRequest, resume: string | undefined) {
12
+ const effort = yield* thinkingLevel(request.effort);
13
+ if (resume !== undefined) {
14
+ yield* Effect.tryPromise({ catch: () => unavailable("Pi session file is unavailable for resume"), try: () => access(resume) });
15
+ }
16
+ const sdk = yield* loadSdk;
17
+ const modelRuntime = yield* Effect.tryPromise({ catch: unavailable, try: () => sdk.ModelRuntime.create() });
18
+ const models = yield* Effect.tryPromise({ catch: unavailable, try: () => modelRuntime.getAvailable() });
19
+ const model = request.model === undefined ? models[0] : models.find((value) => `${value.provider}/${value.id}` === request.model);
20
+ if (model === undefined) {
21
+ return yield* Effect.fail(
22
+ rejected(`Pi offers no model ${request.model ?? "(default)"} with credentials; choose provider/model-id from its catalog`),
23
+ );
24
+ }
25
+ const session = yield* Effect.acquireRelease(
26
+ Effect.tryPromise({
27
+ catch: unavailable,
28
+ try: async () => {
29
+ const agentDir = sdk.getAgentDir();
30
+ const resourceLoader = new sdk.DefaultResourceLoader({
31
+ agentDir,
32
+ cwd: request.cwd,
33
+ ...resourceOptions(options),
34
+ });
35
+ await resourceLoader.reload();
36
+ const { session: opened } = await sdk.createAgentSession({
37
+ agentDir,
38
+ customTools: [],
39
+ cwd: request.cwd,
40
+ model,
41
+ modelRuntime,
42
+ resourceLoader,
43
+ sessionManager: resume === undefined ? sdk.SessionManager.create(request.cwd) : sdk.SessionManager.open(resume, undefined, request.cwd),
44
+ ...(options.constrainedPrompt === undefined ? {} : { tools: [] }),
45
+ ...(effort === undefined ? {} : { thinkingLevel: effort }),
46
+ });
47
+ return opened;
48
+ },
49
+ }),
50
+ (openedSession: AgentSession) => Effect.promise(() => openedSession.abort()).pipe(Effect.ensuring(Effect.sync(() => openedSession.dispose()))),
51
+ );
52
+ if (session.sessionFile === undefined) {
53
+ return yield* Effect.fail(unavailable("Pi opened a session without a persisted file reference"));
54
+ }
55
+ return { session, sessionFile: session.sessionFile };
56
+ });
57
+
58
+ function resourceOptions(options: PiOptions) {
59
+ if (options.constrainedPrompt === undefined) {
60
+ return { additionalSkillPaths: [...(options.skills ?? [])] };
61
+ }
62
+ return {
63
+ noContextFiles: true,
64
+ noExtensions: true,
65
+ noPromptTemplates: true,
66
+ noSkills: true,
67
+ noThemes: true,
68
+ systemPrompt: options.constrainedPrompt,
69
+ };
70
+ }
@@ -0,0 +1,5 @@
1
+ import type { Input } from "@shivaedev/agent/run.ts";
2
+
3
+ export function textOf(input: Input) {
4
+ return input.parts.map((part) => part.text).join("\n");
5
+ }
@@ -0,0 +1,75 @@
1
+ import { Effect, Exit, Scope, Stream } from "effect";
2
+ import { UnknownAcceptance } from "@shivaedev/agent/failures.ts";
3
+ import type { Receipt, Reconciliation, StartRequest } from "@shivaedev/agent/run.ts";
4
+ import { protocolFailure } from "#adapters/failure.ts";
5
+ import { openSession } from "#adapters/openSession.ts";
6
+ import { textOf } from "#adapters/textOf.ts";
7
+ import type { PiOptions } from "#PiOptions.ts";
8
+
9
+ export const makeOperations = Effect.fnUntraced(function* (options: PiOptions) {
10
+ const runs = yield* Effect.sync(() => new Map<string, Effect.Success<ReturnType<typeof openSession>>>());
11
+ const evidence = new Map<string, Reconciliation>();
12
+ function lookup(receipt: Receipt) {
13
+ return Effect.suspend(() => {
14
+ const run = runs.get(receipt.operationId);
15
+ return receipt.backend === "pi" && receipt.reference.version === 1 && run?.receipt.reference.value === receipt.reference.value
16
+ ? Effect.succeed(run)
17
+ : Effect.fail(protocolFailure("run is not open in this backend scope"));
18
+ });
19
+ }
20
+ const submit = Effect.fnUntraced(function* (request: StartRequest, resume: string | undefined) {
21
+ const run = yield* openSession(options, request, resume);
22
+ yield* run.prompt(textOf(request.input), "steer").pipe(
23
+ Effect.mapError((error) => {
24
+ if (error._tag === "Rejected") {
25
+ evidence.set(request.operationId, { evidence: error.message, operationId: request.operationId, type: "not-accepted" });
26
+ return error;
27
+ }
28
+ return new UnknownAcceptance({ backend: "pi", message: error.message, operationId: request.operationId });
29
+ }),
30
+ );
31
+ runs.set(request.operationId, run);
32
+ evidence.set(request.operationId, { receipt: run.receipt, type: "accepted" });
33
+ yield* run.acknowledge;
34
+ yield* Effect.addFinalizer(() =>
35
+ Effect.sync(() => {
36
+ runs.delete(request.operationId);
37
+ }),
38
+ );
39
+ return run.receipt;
40
+ });
41
+ const open = Effect.fnUntraced(function* (request: StartRequest, resume: string | undefined) {
42
+ if (evidence.has(request.operationId)) {
43
+ return yield* Effect.fail(protocolFailure("operation already attempted; reconcile instead"));
44
+ }
45
+ evidence.set(request.operationId, { operationId: request.operationId, reason: "submission has no acceptance evidence yet", type: "uncertain" });
46
+ const runScope = yield* Scope.fork(yield* Effect.scope);
47
+ return yield* submit(request, resume).pipe(
48
+ Effect.provideService(Scope.Scope, runScope),
49
+ Effect.onError((cause) => Scope.close(runScope, Exit.failCause(cause))),
50
+ );
51
+ });
52
+ return {
53
+ events: (receipt: Receipt) => Stream.unwrap(lookup(receipt).pipe(Effect.map((run) => run.events))),
54
+ inspect: (receipt: Receipt) =>
55
+ lookup(receipt).pipe(
56
+ Effect.flatMap((run) => run.inspect),
57
+ Effect.catchTag("ProtocolFailure", () =>
58
+ Effect.succeed({ reason: "no live execution evidence in this backend scope", type: "missing" as const }),
59
+ ),
60
+ ),
61
+ lookup,
62
+ open,
63
+ reconcile: (operationId: string) =>
64
+ Effect.sync(
65
+ (): Reconciliation =>
66
+ evidence.get(operationId) ?? {
67
+ operationId,
68
+ reason: "Pi has no operation-id lookup; missing local evidence does not prove non-acceptance",
69
+ type: "uncertain",
70
+ },
71
+ ),
72
+ resume: (reference: { readonly value: string; readonly version: number }, request: StartRequest) =>
73
+ reference.version === 1 ? open(request, reference.value) : Effect.fail(protocolFailure("unsupported session reference version")),
74
+ };
75
+ });
package/src/pi.ts CHANGED
@@ -1 +1,44 @@
1
- export {};
1
+ import { Effect, Layer } from "effect";
2
+ import { Backend, type ConfiguredBackend } from "@shivaedev/agent/backend.ts";
3
+ import { listPiModels } from "#adapters/listPiModels.ts";
4
+ import { textOf } from "#adapters/textOf.ts";
5
+ import { makeOperations } from "#makeOperations.ts";
6
+ import type { PiOptions } from "#PiOptions.ts";
7
+
8
+ export function pi(options: PiOptions = {}): Layer.Layer<Backend> {
9
+ return Layer.effect(
10
+ Backend,
11
+ Effect.gen(function* (): Effect.fn.Return<ConfiguredBackend> {
12
+ const operations = yield* makeOperations(options);
13
+ return {
14
+ backend: "pi",
15
+ events: operations.events,
16
+ features: {
17
+ imageInput: { reason: "only text input is implemented", support: "unsupported" },
18
+ interruption: {
19
+ implementation: { interrupt: (receipt) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.interrupt)) },
20
+ support: "supported",
21
+ },
22
+ models: { implementation: { list: listPiModels }, support: "supported" },
23
+ queuedInput: {
24
+ implementation: {
25
+ queue: (receipt, input) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.prompt(textOf(input), "followUp"))),
26
+ },
27
+ support: "supported",
28
+ },
29
+ sessions: { implementation: { resume: operations.resume }, support: "supported" },
30
+ steering: {
31
+ implementation: {
32
+ steer: (receipt, input) => operations.lookup(receipt).pipe(Effect.flatMap((run) => run.prompt(textOf(input), "steer"))),
33
+ },
34
+ support: "supported",
35
+ },
36
+ transcript: { reason: "no provider history reader is implemented", support: "unsupported" },
37
+ },
38
+ inspect: operations.inspect,
39
+ reconcile: operations.reconcile,
40
+ start: (request) => operations.open(request, undefined),
41
+ };
42
+ }),
43
+ );
44
+ }