@shivaedev/agent-claude-code 0.2.0 → 0.4.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 (148) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +71 -1
  3. package/dist/SessionConnection.d.ts +10 -0
  4. package/dist/SessionConnection.d.ts.map +1 -0
  5. package/dist/SessionConnection.js +2 -0
  6. package/dist/SessionConnection.js.map +1 -0
  7. package/dist/adapters/ClaudeProvider.d.ts +15 -0
  8. package/dist/adapters/ClaudeProvider.d.ts.map +1 -0
  9. package/dist/adapters/ClaudeProvider.js +6 -0
  10. package/dist/adapters/ClaudeProvider.js.map +1 -0
  11. package/dist/adapters/InputQueue.d.ts +11 -0
  12. package/dist/adapters/InputQueue.d.ts.map +1 -0
  13. package/dist/adapters/InputQueue.js +84 -0
  14. package/dist/adapters/InputQueue.js.map +1 -0
  15. package/dist/adapters/blocks.d.ts +7 -0
  16. package/dist/adapters/blocks.d.ts.map +1 -0
  17. package/dist/adapters/blocks.js +57 -0
  18. package/dist/adapters/blocks.js.map +1 -0
  19. package/dist/adapters/consumeSession.d.ts +8 -0
  20. package/dist/adapters/consumeSession.d.ts.map +1 -0
  21. package/dist/adapters/consumeSession.js +26 -0
  22. package/dist/adapters/consumeSession.js.map +1 -0
  23. package/dist/adapters/loadClaudeSdk.d.ts +4 -0
  24. package/dist/adapters/loadClaudeSdk.d.ts.map +1 -0
  25. package/dist/adapters/loadClaudeSdk.js +7 -0
  26. package/dist/adapters/loadClaudeSdk.js.map +1 -0
  27. package/dist/adapters/mirrorKeys.d.ts +10 -0
  28. package/dist/adapters/mirrorKeys.d.ts.map +1 -0
  29. package/dist/adapters/mirrorKeys.js +26 -0
  30. package/dist/adapters/mirrorKeys.js.map +1 -0
  31. package/dist/adapters/openSession.d.ts +23 -0
  32. package/dist/adapters/openSession.d.ts.map +1 -0
  33. package/dist/adapters/openSession.js +78 -0
  34. package/dist/adapters/openSession.js.map +1 -0
  35. package/dist/adapters/openSessionMapping.d.ts +9 -0
  36. package/dist/adapters/openSessionMapping.d.ts.map +1 -0
  37. package/dist/adapters/openSessionMapping.js +65 -0
  38. package/dist/adapters/openSessionMapping.js.map +1 -0
  39. package/dist/adapters/openSubsessions.d.ts +10 -0
  40. package/dist/adapters/openSubsessions.d.ts.map +1 -0
  41. package/dist/adapters/openSubsessions.js +45 -0
  42. package/dist/adapters/openSubsessions.js.map +1 -0
  43. package/dist/adapters/openTurnUsage.d.ts +9 -0
  44. package/dist/adapters/openTurnUsage.d.ts.map +1 -0
  45. package/dist/adapters/openTurnUsage.js +40 -0
  46. package/dist/adapters/openTurnUsage.js.map +1 -0
  47. package/dist/adapters/openWorkflowNodes.d.ts +13 -0
  48. package/dist/adapters/openWorkflowNodes.d.ts.map +1 -0
  49. package/dist/adapters/openWorkflowNodes.js +60 -0
  50. package/dist/adapters/openWorkflowNodes.js.map +1 -0
  51. package/dist/adapters/rateLimitEvent.d.ts +7 -0
  52. package/dist/adapters/rateLimitEvent.d.ts.map +1 -0
  53. package/dist/adapters/rateLimitEvent.js +27 -0
  54. package/dist/adapters/rateLimitEvent.js.map +1 -0
  55. package/dist/adapters/rawPayload.d.ts +5 -0
  56. package/dist/adapters/rawPayload.d.ts.map +1 -0
  57. package/dist/adapters/rawPayload.js +12 -0
  58. package/dist/adapters/rawPayload.js.map +1 -0
  59. package/dist/adapters/repairSession.d.ts +5 -0
  60. package/dist/adapters/repairSession.d.ts.map +1 -0
  61. package/dist/adapters/repairSession.js +17 -0
  62. package/dist/adapters/repairSession.js.map +1 -0
  63. package/dist/adapters/sessionLanes.d.ts +22 -0
  64. package/dist/adapters/sessionLanes.d.ts.map +1 -0
  65. package/dist/adapters/sessionLanes.js +100 -0
  66. package/dist/adapters/sessionLanes.js.map +1 -0
  67. package/dist/adapters/sessionOptions.d.ts +6 -0
  68. package/dist/adapters/sessionOptions.d.ts.map +1 -0
  69. package/dist/adapters/sessionOptions.js +30 -0
  70. package/dist/adapters/sessionOptions.js.map +1 -0
  71. package/dist/adapters/spilledPreview.d.ts +5 -0
  72. package/dist/adapters/spilledPreview.d.ts.map +1 -0
  73. package/dist/adapters/spilledPreview.js +22 -0
  74. package/dist/adapters/spilledPreview.js.map +1 -0
  75. package/dist/adapters/subsessionEndings.d.ts +23 -0
  76. package/dist/adapters/subsessionEndings.d.ts.map +1 -0
  77. package/dist/adapters/subsessionEndings.js +54 -0
  78. package/dist/adapters/subsessionEndings.js.map +1 -0
  79. package/dist/adapters/systemEvents.d.ts +9 -0
  80. package/dist/adapters/systemEvents.d.ts.map +1 -0
  81. package/dist/adapters/systemEvents.js +39 -0
  82. package/dist/adapters/systemEvents.js.map +1 -0
  83. package/dist/adapters/toolIdentity.d.ts +5 -0
  84. package/dist/adapters/toolIdentity.d.ts.map +1 -0
  85. package/dist/adapters/toolIdentity.js +10 -0
  86. package/dist/adapters/toolIdentity.js.map +1 -0
  87. package/dist/adapters/toolServer.d.ts +4 -0
  88. package/dist/adapters/toolServer.d.ts.map +1 -0
  89. package/dist/adapters/toolServer.js +30 -0
  90. package/dist/adapters/toolServer.js.map +1 -0
  91. package/dist/adapters/transcriptEvents.d.ts +5 -0
  92. package/dist/adapters/transcriptEvents.d.ts.map +1 -0
  93. package/dist/adapters/transcriptEvents.js +18 -0
  94. package/dist/adapters/transcriptEvents.js.map +1 -0
  95. package/dist/adapters/workflowAdoption.d.ts +13 -0
  96. package/dist/adapters/workflowAdoption.d.ts.map +1 -0
  97. package/dist/adapters/workflowAdoption.js +48 -0
  98. package/dist/adapters/workflowAdoption.js.map +1 -0
  99. package/dist/adapters/workflowIdentity.d.ts +22 -0
  100. package/dist/adapters/workflowIdentity.d.ts.map +1 -0
  101. package/dist/adapters/workflowIdentity.js +50 -0
  102. package/dist/adapters/workflowIdentity.js.map +1 -0
  103. package/dist/claudeCode.d.ts +6 -1
  104. package/dist/claudeCode.d.ts.map +1 -1
  105. package/dist/claudeCode.js +101 -1
  106. package/dist/claudeCode.js.map +1 -1
  107. package/dist/options.d.ts +17 -0
  108. package/dist/options.d.ts.map +1 -0
  109. package/dist/options.js +2 -0
  110. package/dist/options.js.map +1 -0
  111. package/dist/protocol.d.ts +7 -0
  112. package/dist/protocol.d.ts.map +1 -0
  113. package/dist/protocol.js +2 -0
  114. package/dist/protocol.js.map +1 -0
  115. package/dist/runState.d.ts +584 -0
  116. package/dist/runState.d.ts.map +1 -0
  117. package/dist/runState.js +52 -0
  118. package/dist/runState.js.map +1 -0
  119. package/package.json +32 -4
  120. package/src/SessionConnection.ts +10 -0
  121. package/src/adapters/ClaudeProvider.ts +16 -0
  122. package/src/adapters/InputQueue.ts +89 -0
  123. package/src/adapters/blocks.ts +63 -0
  124. package/src/adapters/consumeSession.ts +35 -0
  125. package/src/adapters/loadClaudeSdk.ts +6 -0
  126. package/src/adapters/mirrorKeys.ts +33 -0
  127. package/src/adapters/openSession.ts +102 -0
  128. package/src/adapters/openSessionMapping.ts +84 -0
  129. package/src/adapters/openSubsessions.ts +70 -0
  130. package/src/adapters/openTurnUsage.ts +52 -0
  131. package/src/adapters/openWorkflowNodes.ts +73 -0
  132. package/src/adapters/rateLimitEvent.ts +45 -0
  133. package/src/adapters/rawPayload.ts +13 -0
  134. package/src/adapters/repairSession.ts +20 -0
  135. package/src/adapters/sessionLanes.ts +127 -0
  136. package/src/adapters/sessionOptions.ts +42 -0
  137. package/src/adapters/spilledPreview.ts +24 -0
  138. package/src/adapters/subsessionEndings.ts +80 -0
  139. package/src/adapters/systemEvents.ts +60 -0
  140. package/src/adapters/toolIdentity.ts +12 -0
  141. package/src/adapters/toolServer.ts +31 -0
  142. package/src/adapters/transcriptEvents.ts +21 -0
  143. package/src/adapters/workflowAdoption.ts +58 -0
  144. package/src/adapters/workflowIdentity.ts +75 -0
  145. package/src/claudeCode.ts +117 -1
  146. package/src/options.ts +19 -0
  147. package/src/protocol.ts +9 -0
  148. package/src/runState.ts +70 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/agent-claude-code",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Claude Code integration for Effect applications",
5
5
  "homepage": "https://github.com/ShivaeDev/platform/tree/main/packages/agent-claude-code#readme",
6
6
  "bugs": {
@@ -19,7 +19,8 @@
19
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": {
@@ -32,12 +33,38 @@
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.4.0"
44
+ },
38
45
  "devDependencies": {
46
+ "@anthropic-ai/claude-agent-sdk": "0.3.312",
47
+ "@effect/vitest": "4.1.0",
48
+ "@modelcontextprotocol/sdk": "1.32.2",
49
+ "@shivaedev/quality": "0.65.0",
50
+ "@shivaedev/test-story": "0.42.0",
39
51
  "@types/node": "24.19.2",
40
- "typescript": "7.0.2"
52
+ "effect": "4.1.0",
53
+ "typescript": "7.0.2",
54
+ "vitest": "5.0.5"
55
+ },
56
+ "peerDependencies": {
57
+ "@anthropic-ai/claude-agent-sdk": "0.3.312",
58
+ "@modelcontextprotocol/sdk": "1.32.2",
59
+ "effect": "4.1.0"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "@anthropic-ai/claude-agent-sdk": {
63
+ "optional": true
64
+ },
65
+ "@modelcontextprotocol/sdk": {
66
+ "optional": true
67
+ }
41
68
  },
42
69
  "engines": {
43
70
  "node": ">=24"
@@ -48,7 +75,8 @@
48
75
  },
49
76
  "scripts": {
50
77
  "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",
78
+ "ready": "pnpm typecheck && pnpm test && pnpm build",
79
+ "test": "vitest run --configLoader native",
52
80
  "typecheck": "tsc --noEmit"
53
81
  }
54
82
  }
@@ -0,0 +1,10 @@
1
+ import type { Effect, Stream } from "effect";
2
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
3
+ import type { BackendUnavailable } from "@shivaedev/agent/failures.ts";
4
+ import type { Input } from "@shivaedev/agent/run.ts";
5
+
6
+ export interface SessionConnection {
7
+ readonly events: Stream.Stream<RunEvent, BackendUnavailable>;
8
+ readonly interrupt: Effect.Effect<void, BackendUnavailable>;
9
+ readonly queue: (input: Input, priority?: "now") => Effect.Effect<void, BackendUnavailable>;
10
+ }
@@ -0,0 +1,16 @@
1
+ import type { getSubagentMessages, listSubagents, Options, Query, SDKMessage, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import { Context, Layer } from "effect";
3
+ import { loadClaudeSdk } from "#adapters/loadClaudeSdk.ts";
4
+ export class ClaudeProvider extends Context.Service<
5
+ ClaudeProvider,
6
+ {
7
+ readonly query: (request: {
8
+ readonly prompt: AsyncIterable<SDKUserMessage>;
9
+ readonly options: Options;
10
+ }) => AsyncIterable<SDKMessage> & Pick<Query, "interrupt" | "close">;
11
+ readonly getSubagentMessages: typeof getSubagentMessages;
12
+ readonly listSubagents: typeof listSubagents;
13
+ }
14
+ >()("@shivaedev/agent-claude-code/ClaudeProvider") {
15
+ static layer = Layer.effect(this, loadClaudeSdk);
16
+ }
@@ -0,0 +1,89 @@
1
+ import type { SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import { Deferred, Effect } from "effect";
3
+ import { BackendUnavailable } from "@shivaedev/agent/failures.ts";
4
+
5
+ interface QueuedInput {
6
+ readonly accepted: Deferred.Deferred<void, BackendUnavailable>;
7
+ readonly message: SDKUserMessage;
8
+ }
9
+ function closedFailure() {
10
+ return new BackendUnavailable({
11
+ backend: "claude-code",
12
+ message: "session closed before delivery reached the provider",
13
+ });
14
+ }
15
+ // Claude accepts a user message when `query` pulls it from the prompt iterable.
16
+ export class InputQueue {
17
+ readonly #buffer: QueuedInput[] = [];
18
+ readonly #handOff: (message: SDKUserMessage) => void;
19
+ #pending: ((result: IteratorResult<SDKUserMessage>) => void) | null = null;
20
+ #done = false;
21
+ constructor(handOff: (message: SDKUserMessage) => void) {
22
+ this.#handOff = handOff;
23
+ }
24
+ #accept(input: QueuedInput): void {
25
+ this.#handOff(input.message);
26
+ Deferred.doneUnsafe(input.accepted, Effect.void);
27
+ }
28
+ #fail(input: QueuedInput): void {
29
+ Deferred.doneUnsafe(input.accepted, Effect.fail(closedFailure()));
30
+ }
31
+ #offer(input: QueuedInput): void {
32
+ if (this.#done) {
33
+ this.#fail(input);
34
+ return;
35
+ }
36
+ const pending = this.#pending;
37
+ if (pending !== null) {
38
+ this.#pending = null;
39
+ pending({ done: false, value: input.message });
40
+ this.#accept(input);
41
+ return;
42
+ }
43
+ this.#buffer.push(input);
44
+ }
45
+ push(message: SDKUserMessage): Effect.Effect<void, BackendUnavailable> {
46
+ const queue = this;
47
+ return Effect.gen(function* () {
48
+ const accepted = yield* Deferred.make<void, BackendUnavailable>();
49
+ yield* Effect.sync(() => queue.#offer({ accepted, message }));
50
+ yield* Deferred.await(accepted);
51
+ });
52
+ }
53
+ close(): void {
54
+ this.#done = true;
55
+ for (const input of this.#buffer.splice(0)) {
56
+ this.#fail(input);
57
+ }
58
+ const pending = this.#pending;
59
+ if (pending !== null) {
60
+ this.#pending = null;
61
+ pending({ done: true, value: undefined });
62
+ }
63
+ }
64
+ stream(): AsyncIterable<SDKUserMessage> {
65
+ return {
66
+ [Symbol.asyncIterator]: () => ({
67
+ next: () => {
68
+ const buffered = this.#buffer.shift();
69
+ if (buffered !== undefined) {
70
+ this.#accept(buffered);
71
+ return Promise.resolve({
72
+ done: false,
73
+ value: buffered.message,
74
+ });
75
+ }
76
+ if (this.#done) {
77
+ return Promise.resolve({
78
+ done: true as const,
79
+ value: undefined,
80
+ });
81
+ }
82
+ return new Promise((promiseResolve) => {
83
+ this.#pending = promiseResolve;
84
+ });
85
+ },
86
+ }),
87
+ };
88
+ }
89
+ }
@@ -0,0 +1,63 @@
1
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
2
+ import { toolIdentity } from "#adapters/toolIdentity.ts";
3
+ import type { Origin, RawPayload } from "#protocol.ts";
4
+ export function isRecord(value: unknown): value is Record<string, unknown> {
5
+ return typeof value === "object" && value !== null;
6
+ }
7
+ export function contentBlocks(message: object): readonly Record<string, unknown>[] {
8
+ if (!("message" in message && isRecord(message.message))) {
9
+ return [];
10
+ }
11
+ const content = message.message.content;
12
+ if (typeof content === "string") {
13
+ return [{ text: content, type: "text" }];
14
+ }
15
+ return Array.isArray(content) ? content.filter(isRecord) : [];
16
+ }
17
+ export function textOf(content: unknown): string {
18
+ if (typeof content === "string") {
19
+ return content;
20
+ }
21
+ if (Array.isArray(content)) {
22
+ return content
23
+ .filter(isRecord)
24
+ .map((block) => (typeof block.text === "string" ? block.text : ""))
25
+ .join("");
26
+ }
27
+ return JSON.stringify(content);
28
+ }
29
+ export function blockEvent(
30
+ raw: RawPayload,
31
+ role: "agent" | "user",
32
+ block: Record<string, unknown>,
33
+ origin: Origin | undefined,
34
+ ): RunEvent | undefined {
35
+ const from = origin === undefined ? {} : { origin };
36
+ if (block.type === "text" && typeof block.text === "string") {
37
+ return { ...from, parts: [{ text: block.text, type: "text" }], raw, role, type: "message" };
38
+ }
39
+ if (block.type === "thinking" && typeof block.thinking === "string") {
40
+ return { ...from, raw, text: block.thinking, type: "thinking" };
41
+ }
42
+ if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
43
+ return {
44
+ ...from,
45
+ input: JSON.stringify(block.input),
46
+ ...toolIdentity(block.name),
47
+ raw,
48
+ toolId: block.id,
49
+ type: "tool.started",
50
+ };
51
+ }
52
+ if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
53
+ return {
54
+ ...from,
55
+ ok: block.is_error !== true,
56
+ output: textOf(block.content),
57
+ raw,
58
+ toolId: block.tool_use_id,
59
+ type: "tool.completed",
60
+ };
61
+ }
62
+ return undefined;
63
+ }
@@ -0,0 +1,35 @@
1
+ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import { Cause, Queue } from "effect";
3
+ import { BackendUnavailable } from "@shivaedev/agent/failures.ts";
4
+ import type { InputQueue } from "#adapters/InputQueue.ts";
5
+ import type { Delivery } from "#adapters/sessionLanes.ts";
6
+ import type { Repair } from "#adapters/workflowAdoption.ts";
7
+
8
+ export async function consumeSession(
9
+ live: AsyncIterable<SDKMessage>,
10
+ input: InputQueue,
11
+ queue: Queue.Queue<Delivery, BackendUnavailable | Cause.Done>,
12
+ deliver: (delivery: Delivery) => void,
13
+ repair: () => Promise<Repair | undefined>,
14
+ stopped: () => boolean,
15
+ ): Promise<void> {
16
+ try {
17
+ for await (const message of live) {
18
+ if (stopped()) {
19
+ break;
20
+ }
21
+ deliver({ kind: "frame", message });
22
+ }
23
+ const recovered = await repair();
24
+ if (!stopped() && recovered !== undefined) {
25
+ deliver({ kind: "repair", repair: recovered });
26
+ }
27
+ } catch (error) {
28
+ if (!stopped()) {
29
+ Queue.failCauseUnsafe(queue, Cause.fail(new BackendUnavailable({ backend: "claude-code", message: String(error) })));
30
+ }
31
+ } finally {
32
+ input.close();
33
+ Queue.endUnsafe(queue);
34
+ }
35
+ }
@@ -0,0 +1,6 @@
1
+ import { Effect } from "effect";
2
+ import { BackendUnavailable } from "@shivaedev/agent/failures.ts";
3
+ export const loadClaudeSdk = Effect.tryPromise({
4
+ catch: (error) => new BackendUnavailable({ backend: "claude-code", message: String(error) }),
5
+ try: () => import("@anthropic-ai/claude-agent-sdk"),
6
+ });
@@ -0,0 +1,33 @@
1
+ import type { SessionKey } from "@anthropic-ai/claude-agent-sdk";
2
+
3
+ const SUBAGENTS = "subagents";
4
+ const WORKFLOWS = "workflows";
5
+ const AGENT = "agent-";
6
+ // Observed transcript keys identify delegated agents by a final `agent-<id>` segment. Workflow keys start with
7
+ // `subagents/workflows/<run-id>` and may nest through further delegates.
8
+ export interface WorkflowAgentRef {
9
+ readonly agentId: string;
10
+ readonly runId: string;
11
+ }
12
+ function agentIdOf(segment = ""): string | undefined {
13
+ return segment.startsWith(AGENT) ? segment.slice(AGENT.length) : undefined;
14
+ }
15
+ export function workflowAgentRef(key: SessionKey): WorkflowAgentRef | undefined {
16
+ const segments = key.subpath?.split("/") ?? [];
17
+ const [scope, kind, runId] = segments;
18
+ if (scope !== SUBAGENTS || kind !== WORKFLOWS || runId === undefined) {
19
+ return undefined;
20
+ }
21
+ const agentId = agentIdOf(segments.at(-1));
22
+ return agentId === undefined || segments.length < 4 ? undefined : { agentId, runId };
23
+ }
24
+ export function subagentRef(key: SessionKey): string | undefined {
25
+ const segments = key.subpath?.split("/") ?? [];
26
+ return segments.length >= 2 && segments[0] === SUBAGENTS ? agentIdOf(segments.at(-1)) : undefined;
27
+ }
28
+ export function agentFileRef(agentId: string): string {
29
+ return [SUBAGENTS, `${AGENT}${agentId}`].join("/");
30
+ }
31
+ export function workflowRunRef(runId: string): string {
32
+ return [SUBAGENTS, WORKFLOWS, runId].join("/");
33
+ }
@@ -0,0 +1,102 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { resolve } from "node:path";
3
+ import type { EffortLevel, SessionStore } from "@anthropic-ai/claude-agent-sdk";
4
+ import { type Cause, Effect, Queue, type Scope, Stream } from "effect";
5
+ import { BackendUnavailable } from "@shivaedev/agent/failures.ts";
6
+ import type { Input, StartRequest } from "@shivaedev/agent/run.ts";
7
+ import { ClaudeProvider } from "#adapters/ClaudeProvider.ts";
8
+ import { consumeSession } from "#adapters/consumeSession.ts";
9
+ import { InputQueue } from "#adapters/InputQueue.ts";
10
+ import { repairSession } from "#adapters/repairSession.ts";
11
+ import { type Delivery, openSessionLanes } from "#adapters/sessionLanes.ts";
12
+ import { sessionOptions } from "#adapters/sessionOptions.ts";
13
+ import { makeToolServer } from "#adapters/toolServer.ts";
14
+ import type { ClaudeOptions } from "#options.ts";
15
+ import type { SessionConnection } from "#SessionConnection.ts";
16
+
17
+ function unavailable(error: unknown) {
18
+ return new BackendUnavailable({ backend: "claude-code", message: String(error) });
19
+ }
20
+
21
+ export const openSession = Effect.fn("ClaudeCode.openSession")(function* (
22
+ request: StartRequest,
23
+ config: ClaudeOptions,
24
+ effort: EffortLevel | undefined,
25
+ resume: string | undefined,
26
+ ): Effect.fn.Return<SessionConnection, BackendUnavailable, ClaudeProvider | Scope.Scope> {
27
+ const peer = yield* ClaudeProvider;
28
+ const deliveries = yield* Queue.unbounded<Delivery, BackendUnavailable | Cause.Done>();
29
+ const tools = config.tools ?? [];
30
+ const server =
31
+ tools.length === 0
32
+ ? undefined
33
+ : yield* Effect.acquireRelease(Effect.tryPromise({ catch: unavailable, try: () => makeToolServer(tools) }), (instance) =>
34
+ Effect.promise(() => instance.close()),
35
+ );
36
+ return yield* Effect.acquireRelease(
37
+ Effect.try({
38
+ catch: unavailable,
39
+ try: () => {
40
+ let nativeRef = resume;
41
+ let stopped = false;
42
+ function deliver(delivery: Delivery) {
43
+ if (delivery.kind === "frame" && delivery.message.type === "system" && delivery.message.subtype === "init") {
44
+ nativeRef = delivery.message.session_id;
45
+ }
46
+ Queue.offerUnsafe(deliveries, delivery);
47
+ }
48
+ const inputIds = new Map<string, string>();
49
+ const input = new InputQueue((message) => {
50
+ const inputId = message.uuid === undefined ? undefined : inputIds.get(message.uuid);
51
+ deliver({ kind: "frame", message, ...(inputId === undefined ? {} : { inputId }) });
52
+ });
53
+ const store: SessionStore = {
54
+ append: (key, entries) => {
55
+ deliver({ kind: "mirror", write: { entries, key } });
56
+ return Promise.resolve();
57
+ },
58
+ load: () => Promise.resolve(null),
59
+ };
60
+ const live = peer.query({
61
+ options: sessionOptions(request, config, store, effort, resume, server),
62
+ prompt: input.stream(),
63
+ });
64
+ void consumeSession(
65
+ live,
66
+ input,
67
+ deliveries,
68
+ deliver,
69
+ () =>
70
+ stopped || nativeRef === undefined
71
+ ? Promise.resolve(undefined)
72
+ : Effect.runPromise(repairSession(nativeRef, resolve(request.cwd)).pipe(Effect.provideService(ClaudeProvider, peer))),
73
+ () => stopped,
74
+ );
75
+ return {
76
+ close: () => {
77
+ stopped = true;
78
+ input.close();
79
+ live.close();
80
+ Queue.endUnsafe(deliveries);
81
+ },
82
+ events: Stream.fromQueue(deliveries).pipe(Stream.map(openSessionLanes().deliver), Stream.flattenIterable),
83
+ interrupt: Effect.tryPromise({ catch: unavailable, try: () => live.interrupt() }).pipe(Effect.asVoid),
84
+ queue: (value: Input, priority?: "now") => {
85
+ const uuid = randomUUID();
86
+ if (value.id !== undefined) {
87
+ inputIds.set(uuid, value.id);
88
+ }
89
+ return input.push({
90
+ message: { content: value.parts.map((part) => part.text).join("\n"), role: "user" },
91
+ "parent_tool_use_id": null,
92
+ type: "user",
93
+ uuid,
94
+ ...(priority === undefined ? {} : { priority }),
95
+ });
96
+ },
97
+ };
98
+ },
99
+ }),
100
+ (session) => Effect.sync(session.close),
101
+ );
102
+ });
@@ -0,0 +1,84 @@
1
+ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
3
+ import { blockEvent, contentBlocks } from "#adapters/blocks.ts";
4
+ import { openSubsessions } from "#adapters/openSubsessions.ts";
5
+ import { openTurnUsage } from "#adapters/openTurnUsage.ts";
6
+ import { rateLimitEvent } from "#adapters/rateLimitEvent.ts";
7
+ import { rawOf } from "#adapters/rawPayload.ts";
8
+ import { spilledPreview } from "#adapters/spilledPreview.ts";
9
+ import { systemEvents } from "#adapters/systemEvents.ts";
10
+ import type { Origin, RawPayload } from "#protocol.ts";
11
+
12
+ type ResultMessage = Extract<
13
+ SDKMessage,
14
+ {
15
+ type: "result";
16
+ }
17
+ >;
18
+ function turnStatus(message: ResultMessage) {
19
+ if (message.terminal_reason === "aborted_streaming") {
20
+ return "interrupted";
21
+ }
22
+ return message.is_error ? "failed" : "completed";
23
+ }
24
+ function originOf(message: SDKMessage): Origin | undefined {
25
+ const spawnedBy = "parent_tool_use_id" in message && typeof message.parent_tool_use_id === "string" ? message.parent_tool_use_id : undefined;
26
+ if (spawnedBy === undefined) {
27
+ return undefined;
28
+ }
29
+ return "parent_agent_id" in message && typeof message.parent_agent_id === "string"
30
+ ? { parentNode: message.parent_agent_id, spawnedBy }
31
+ : { spawnedBy };
32
+ }
33
+ type ContentMessage = Extract<
34
+ SDKMessage,
35
+ {
36
+ type: "assistant" | "user";
37
+ }
38
+ >;
39
+ function contentEvents(raw: RawPayload, message: ContentMessage, lifecycle: readonly RunEvent[]): readonly RunEvent[] {
40
+ const role = message.type === "assistant" ? "agent" : "user";
41
+ const origin = originOf(message);
42
+ const events = [
43
+ ...contentBlocks(message)
44
+ .map((block) => blockEvent(raw, role, block, origin))
45
+ .filter((event): event is RunEvent => event !== undefined),
46
+ ...spilledPreview(raw, message, origin),
47
+ ...lifecycle,
48
+ ];
49
+ return events.length === 0 ? [{ raw, type: "raw" }] : events;
50
+ }
51
+ interface SessionMapping {
52
+ readonly frame: (message: SDKMessage) => readonly RunEvent[];
53
+ readonly spawnerOf: (childRef: string) => string | undefined;
54
+ }
55
+ export function openSessionMapping(): SessionMapping {
56
+ const subsessions = openSubsessions();
57
+ const turns = openTurnUsage();
58
+ function frame(message: SDKMessage): readonly RunEvent[] {
59
+ const raw = rawOf(message);
60
+ const system = message.type === "system" ? systemEvents(raw, message) : undefined;
61
+ if (system !== undefined) {
62
+ return system;
63
+ }
64
+ if (message.type === "rate_limit_event") {
65
+ return [rateLimitEvent(raw, message)];
66
+ }
67
+ if (message.type === "result") {
68
+ return [
69
+ ...turns.usage(raw, message),
70
+ {
71
+ raw,
72
+ state: turnStatus(message),
73
+ type: "execution",
74
+ },
75
+ ];
76
+ }
77
+ const lifecycle = subsessions.events(raw, message);
78
+ if (message.type === "assistant" || message.type === "user") {
79
+ return contentEvents(raw, message, lifecycle);
80
+ }
81
+ return lifecycle.length === 0 ? [{ raw, type: "raw" }] : lifecycle;
82
+ }
83
+ return { frame, spawnerOf: subsessions.spawnerOf };
84
+ }
@@ -0,0 +1,70 @@
1
+ import type { SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
3
+ import { type Ending, endedEvent, notifiedEnding, reportedEnding, updatedEnding } from "#adapters/subsessionEndings.ts";
4
+ import type { RawPayload } from "#protocol.ts";
5
+
6
+ const LOCAL_AGENT = "local_agent";
7
+ type SystemMessage = Extract<
8
+ SDKMessage,
9
+ {
10
+ type: "system";
11
+ }
12
+ >;
13
+ type TaskStarted = Extract<
14
+ SystemMessage,
15
+ {
16
+ subtype: "task_started";
17
+ }
18
+ >;
19
+ type Opened = Extract<
20
+ RunEvent,
21
+ {
22
+ type: "child.opened";
23
+ }
24
+ >;
25
+ function openedEvent(raw: RawPayload, message: TaskStarted): Opened | undefined {
26
+ if (message.task_type !== LOCAL_AGENT || message.tool_use_id === undefined) {
27
+ return undefined;
28
+ }
29
+ return {
30
+ ...(message.prompt === undefined ? {} : { charter: message.prompt }),
31
+ ...(message.description === "" ? {} : { label: message.description }),
32
+ childRef: message.task_id,
33
+ raw,
34
+ spawnedBy: message.tool_use_id,
35
+ type: "child.opened",
36
+ };
37
+ }
38
+ interface Subsessions {
39
+ readonly events: (raw: RawPayload, message: SDKMessage) => readonly RunEvent[];
40
+ readonly spawnerOf: (childRef: string) => string | undefined;
41
+ }
42
+ // Task updates and notifications omit `task_type`, so only ids observed opening as `local_agent` can close a subsession.
43
+ export function openSubsessions(): Subsessions {
44
+ const open = new Set<string>();
45
+ const spawners = new Map<string, string>();
46
+ function close(raw: RawPayload, ending: Ending | undefined): readonly RunEvent[] {
47
+ return ending === undefined || !open.delete(ending.childRef) ? [] : [endedEvent(raw, ending)];
48
+ }
49
+ return {
50
+ events: (raw, message) => {
51
+ if (message.type !== "system") {
52
+ return close(raw, reportedEnding(message));
53
+ }
54
+ if (message.subtype === "task_started") {
55
+ const opened = openedEvent(raw, message);
56
+ if (opened === undefined) {
57
+ return [];
58
+ }
59
+ open.add(message.task_id);
60
+ spawners.set(message.task_id, opened.spawnedBy);
61
+ return [opened];
62
+ }
63
+ if (message.subtype === "task_updated") {
64
+ return close(raw, updatedEnding(message));
65
+ }
66
+ return message.subtype === "task_notification" ? close(raw, notifiedEnding(message)) : [];
67
+ },
68
+ spawnerOf: (childRef) => spawners.get(childRef),
69
+ };
70
+ }
@@ -0,0 +1,52 @@
1
+ import type { ModelUsage, SDKMessage } from "@anthropic-ai/claude-agent-sdk";
2
+ import type { RunEvent } from "@shivaedev/agent/events.ts";
3
+ import type { RawPayload } from "#protocol.ts";
4
+
5
+ function step(now: number, before: number) {
6
+ return now < before ? now : now - before;
7
+ }
8
+ export function openTurnUsage() {
9
+ const previous = new Map<string, ModelUsage>();
10
+ return {
11
+ usage: (
12
+ raw: RawPayload,
13
+ message: Extract<
14
+ SDKMessage,
15
+ {
16
+ type: "result";
17
+ }
18
+ >,
19
+ ): readonly RunEvent[] =>
20
+ Object.entries(message.modelUsage).flatMap(([key, now]): RunEvent[] => {
21
+ if (now.inputTokens + now.outputTokens + now.cacheReadInputTokens + now.cacheCreationInputTokens + now.costUSD === 0) {
22
+ return [];
23
+ }
24
+ const before = previous.get(key);
25
+ previous.set(key, now);
26
+ const model = now.canonicalModel ?? key;
27
+ return [
28
+ {
29
+ cacheReadTokens: step(now.cacheReadInputTokens, before?.cacheReadInputTokens ?? 0),
30
+ cacheWriteTokens: step(now.cacheCreationInputTokens, before?.cacheCreationInputTokens ?? 0),
31
+ inputTokens: step(now.inputTokens, before?.inputTokens ?? 0),
32
+ model,
33
+ outputTokens: step(now.outputTokens, before?.outputTokens ?? 0),
34
+ raw,
35
+ scope: "delta",
36
+ type: "usage",
37
+ unit: "turn",
38
+ },
39
+ {
40
+ amount: step(now.costUSD, before?.costUSD ?? 0),
41
+ currency: "USD",
42
+ model,
43
+ raw,
44
+ scope: "delta",
45
+ source: "provider",
46
+ type: "cost",
47
+ unit: "turn",
48
+ },
49
+ ];
50
+ }),
51
+ };
52
+ }