@xandreed/core 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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +5 -0
  3. package/dist/domain/agent-failure.entity.d.ts +12 -0
  4. package/dist/domain/agent-failure.entity.functions.d.ts +5 -0
  5. package/dist/domain/agent-failure.entity.functions.js +8 -0
  6. package/dist/domain/agent-failure.entity.js +8 -0
  7. package/dist/domain/failure.entity.d.ts +12 -0
  8. package/dist/domain/failure.entity.functions.d.ts +3 -0
  9. package/dist/domain/failure.entity.functions.js +1 -0
  10. package/dist/domain/failure.entity.js +5 -0
  11. package/dist/domain/loop-event.entity.d.ts +94 -0
  12. package/dist/domain/loop-event.entity.js +0 -0
  13. package/dist/domain/message.entity.d.ts +227 -0
  14. package/dist/domain/message.entity.js +51 -0
  15. package/dist/domain/model-call-policy.entity.d.ts +12 -0
  16. package/dist/domain/model-call-policy.entity.js +5 -0
  17. package/dist/domain/model-catalog.entity.d.ts +10 -0
  18. package/dist/domain/model-catalog.entity.js +7 -0
  19. package/dist/domain/model-selection.entity.d.ts +24 -0
  20. package/dist/domain/model-selection.entity.functions.d.ts +5 -0
  21. package/dist/domain/model-selection.entity.functions.js +9 -0
  22. package/dist/domain/model-selection.entity.js +8 -0
  23. package/dist/domain/prompt-provenance.entity.d.ts +11 -0
  24. package/dist/domain/prompt-provenance.entity.js +8 -0
  25. package/dist/domain/token-usage.entity.d.ts +9 -0
  26. package/dist/domain/token-usage.entity.functions.d.ts +3 -0
  27. package/dist/domain/token-usage.entity.functions.js +11 -0
  28. package/dist/domain/token-usage.entity.js +7 -0
  29. package/dist/harness/capability.entity.d.ts +61 -0
  30. package/dist/harness/capability.entity.functions.d.ts +6 -0
  31. package/dist/harness/capability.entity.functions.js +10 -0
  32. package/dist/harness/capability.entity.js +26 -0
  33. package/dist/harness/capability.test.d.ts +1 -0
  34. package/dist/harness/config.entity.d.ts +48 -0
  35. package/dist/harness/config.entity.functions.d.ts +3 -0
  36. package/dist/harness/config.entity.functions.js +1 -0
  37. package/dist/harness/config.entity.js +18 -0
  38. package/dist/harness/plugin.adapter.d.ts +18 -0
  39. package/dist/harness/plugin.adapter.js +13 -0
  40. package/dist/harness/plugin.entity.d.ts +25 -0
  41. package/dist/harness/plugin.entity.js +9 -0
  42. package/dist/harness/session.entity.d.ts +33 -0
  43. package/dist/harness/session.entity.js +25 -0
  44. package/dist/index.d.ts +51 -0
  45. package/dist/index.js +99 -0
  46. package/dist/loop/cacheKey.d.ts +13 -0
  47. package/dist/loop/cacheKey.js +2 -0
  48. package/dist/loop/mapping.d.ts +79 -0
  49. package/dist/loop/mapping.js +154 -0
  50. package/dist/loop/mapping.test.d.ts +1 -0
  51. package/dist/loop/modelPolicy.d.ts +21 -0
  52. package/dist/loop/modelPolicy.js +2 -0
  53. package/dist/loop/promptProvenance.d.ts +8 -0
  54. package/dist/loop/promptProvenance.js +2 -0
  55. package/dist/loop/stepContext.d.ts +3 -0
  56. package/dist/loop/stepContext.js +2 -0
  57. package/dist/loop/streamFold.d.ts +31 -0
  58. package/dist/loop/streamFold.js +58 -0
  59. package/dist/loop/streamFold.test.d.ts +1 -0
  60. package/dist/mcp/bridge.d.ts +51 -0
  61. package/dist/mcp/bridge.js +70 -0
  62. package/dist/mcp/bridge.test.d.ts +1 -0
  63. package/dist/ports/auth-store.port.d.ts +50 -0
  64. package/dist/ports/auth-store.port.js +18 -0
  65. package/dist/ports/capability.port.d.ts +15 -0
  66. package/dist/ports/capability.port.js +7 -0
  67. package/dist/ports/conversation-store.port.d.ts +250 -0
  68. package/dist/ports/conversation-store.port.js +36 -0
  69. package/dist/ports/file-system.port.d.ts +26 -0
  70. package/dist/ports/file-system.port.js +10 -0
  71. package/dist/ports/harness.port.d.ts +106 -0
  72. package/dist/ports/harness.port.js +31 -0
  73. package/dist/ports/mcp-client.port.d.ts +74 -0
  74. package/dist/ports/mcp-client.port.js +24 -0
  75. package/dist/ports/model-catalog.port.d.ts +10 -0
  76. package/dist/ports/model-catalog.port.js +4 -0
  77. package/dist/ports/settings-store.port.d.ts +85 -0
  78. package/dist/ports/settings-store.port.js +36 -0
  79. package/dist/ports/shell.port.d.ts +44 -0
  80. package/dist/ports/shell.port.js +16 -0
  81. package/dist/ports/utility-llm.port.d.ts +48 -0
  82. package/dist/ports/utility-llm.port.js +16 -0
  83. package/dist/session/chassis.d.ts +59 -0
  84. package/dist/session/chassis.js +27 -0
  85. package/dist/session/chassis.test.d.ts +1 -0
  86. package/dist/spec/SpecDoc.d.ts +162 -0
  87. package/dist/spec/SpecDoc.js +59 -0
  88. package/dist/spec/codec.d.ts +30 -0
  89. package/dist/spec/codec.js +123 -0
  90. package/dist/spec/codec.test.d.ts +1 -0
  91. package/dist/spec/frontmatter.d.ts +16 -0
  92. package/dist/spec/frontmatter.js +25 -0
  93. package/dist/util/json.d.ts +19 -0
  94. package/dist/util/json.js +15 -0
  95. package/package.json +30 -0
@@ -0,0 +1,154 @@
1
+ import { Option } from "effect";
2
+ import { ToolCallId } from "../domain/message.entity.js";
3
+ export const handoffToMessage = (summary) => ({
4
+ role: "user",
5
+ content: `[System note: the earlier history of this conversation has been handed off into the summary below to free up context. Treat the summary as the source of truth for everything before this point.]
6
+
7
+ ` + summary
8
+ }), safeKeepFrom = (messages, keepTurns) => {
9
+ const assistantIndexes = messages.flatMap((message, index) => message.role === "assistant" ? [index] : []);
10
+ if (assistantIndexes.length <= keepTurns)
11
+ return Option.none();
12
+ const cut = assistantIndexes[assistantIndexes.length - keepTurns];
13
+ return cut === void 0 || cut <= 0 ? Option.none() : Option.some(cut);
14
+ };
15
+ const hasKeys = (o) => typeof o === "object" && o !== null && Object.keys(o).length > 0, withOptions = (blob) => hasKeys(blob) ? { options: blob } : {}, withProviderOptions = (blob) => hasKeys(blob) ? { providerOptions: blob } : {}, USAGE_KEY = "engine", withoutEngineStamp = (blob) => {
16
+ if (!hasKeys(blob))
17
+ return blob;
18
+ const { [USAGE_KEY]: _engine, ...rest } = blob;
19
+ return rest;
20
+ };
21
+ export const toPromptMessages = (messages) => messages.map((m) => {
22
+ if (m.role === "user")
23
+ return { role: "user", content: m.content };
24
+ if (m.role === "assistant")
25
+ return {
26
+ role: "assistant",
27
+ content: m.content.map((p) => {
28
+ if (p.type === "text")
29
+ return { type: "text", text: p.text, ...withOptions(p.providerOptions) };
30
+ if (p.type === "reasoning")
31
+ return { type: "reasoning", text: p.text, ...withOptions(p.providerOptions) };
32
+ return {
33
+ type: "tool-call",
34
+ id: p.toolCallId,
35
+ name: p.toolName,
36
+ params: p.input ?? {},
37
+ providerExecuted: p.providerExecuted ?? !1,
38
+ ...withOptions(p.providerOptions)
39
+ };
40
+ }),
41
+ ...withOptions(withoutEngineStamp(m.providerOptions))
42
+ };
43
+ return {
44
+ role: "tool",
45
+ content: m.content.map((p) => ({
46
+ type: "tool-result",
47
+ id: p.toolCallId,
48
+ name: p.toolName,
49
+ result: p.output,
50
+ isFailure: p.isError ?? !1,
51
+ providerExecuted: !1,
52
+ ...withOptions(p.providerOptions)
53
+ }))
54
+ };
55
+ }), withToolCallIds = (content, turnIndex) => content.reduce((acc, part) => {
56
+ const p = part, bare = p.id === void 0 || p.id === "";
57
+ if (p.type === "tool-call")
58
+ return {
59
+ calls: acc.calls + 1,
60
+ results: acc.results,
61
+ out: [
62
+ ...acc.out,
63
+ bare ? { ...p, id: `${turnIndex}:${p.name ?? ""}:${acc.calls}` } : part
64
+ ]
65
+ };
66
+ if (p.type === "tool-result")
67
+ return {
68
+ calls: acc.calls,
69
+ results: acc.results + 1,
70
+ out: [
71
+ ...acc.out,
72
+ bare ? { ...p, id: `${turnIndex}:${p.name ?? ""}:${acc.results}` } : part
73
+ ]
74
+ };
75
+ return { ...acc, out: [...acc.out, part] };
76
+ }, { calls: 0, results: 0, out: [] }).out, responseToAgentMessages = (content) => {
77
+ const parts = content, assistantParts = parts.flatMap((p) => {
78
+ if (p.type === "text")
79
+ return [{ type: "text", text: p.text ?? "", ...withProviderOptions(p.metadata) }];
80
+ if (p.type === "reasoning")
81
+ return [
82
+ { type: "reasoning", text: p.text ?? "", ...withProviderOptions(p.metadata) }
83
+ ];
84
+ if (p.type === "tool-call")
85
+ return [
86
+ {
87
+ type: "tool-call",
88
+ toolCallId: ToolCallId.make(p.id ?? ""),
89
+ toolName: p.name ?? "",
90
+ input: p.params ?? {},
91
+ providerExecuted: p.providerExecuted ?? !1,
92
+ ...withProviderOptions(p.metadata)
93
+ }
94
+ ];
95
+ return [];
96
+ }), toolParts = parts.flatMap((p) => p.type === "tool-result" ? [
97
+ {
98
+ type: "tool-result",
99
+ toolCallId: ToolCallId.make(p.id ?? ""),
100
+ toolName: p.name ?? "",
101
+ output: p.result,
102
+ isError: p.isFailure ?? !1,
103
+ ...withProviderOptions(p.metadata)
104
+ }
105
+ ] : []);
106
+ return [
107
+ ...assistantParts.length > 0 ? [{ role: "assistant", content: assistantParts }] : [],
108
+ ...toolParts.length > 0 ? [{ role: "tool", content: toolParts }] : []
109
+ ];
110
+ }, responseText = (content) => content.filter((p) => p.type === "text").map((p) => p.text ?? "").join(""), responseReasoning = (content) => content.filter((p) => p.type === "reasoning").map((p) => p.text ?? "").join("").trim(), responseToolCalls = (content) => content.filter((p) => p.type === "tool-call").map((p) => ({ id: p.id ?? "", toolName: p.name ?? "", args: p.params ?? {} })), responseToolResults = (content) => content.filter((p) => p.type === "tool-result").map((p) => ({
111
+ id: p.id ?? "",
112
+ toolName: p.name ?? "",
113
+ ok: !(p.isFailure ?? !1),
114
+ result: p.result
115
+ })), extractUsage = (usage, content) => {
116
+ const u = usage ?? {}, finishes = content.filter((p) => p.type === "finish"), finish = finishes.find((p) => {
117
+ const fu = p.usage;
118
+ return fu?.inputTokens !== void 0 || fu?.outputTokens !== void 0;
119
+ }) ?? finishes[0], gm = finish?.metadata?.google?.usageMetadata, fu = finish?.usage, inputTokens = u.inputTokens ?? fu?.inputTokens ?? gm?.promptTokenCount ?? 0, outputTokens = u.outputTokens ?? fu?.outputTokens ?? gm?.candidatesTokenCount ?? 0, totalTokens = u.totalTokens ?? fu?.totalTokens ?? gm?.totalTokenCount ?? 0, cacheReadTokens = u.cachedInputTokens ?? fu?.cachedInputTokens ?? gm?.cachedContentTokenCount ?? 0, au = finish?.metadata?.anthropic?.usage;
120
+ if (au !== void 0 && au !== null && typeof au === "object") {
121
+ const cacheRead = au.cache_read_input_tokens ?? 0, cacheWrite = au.cache_creation_input_tokens ?? 0, fullInput = inputTokens + cacheRead + cacheWrite;
122
+ return {
123
+ inputTokens: fullInput,
124
+ outputTokens,
125
+ totalTokens: fullInput + outputTokens,
126
+ cacheReadTokens: cacheRead
127
+ };
128
+ }
129
+ return { inputTokens, outputTokens, totalTokens, cacheReadTokens };
130
+ }, withUsageOnAssistant = (messages, usage, model = Option.none()) => {
131
+ const at = messages.findIndex((m) => m.role === "assistant"), stamp = {
132
+ ...usage,
133
+ ...Option.match(model, { onNone: () => ({}), onSome: (id) => ({ model: id }) })
134
+ };
135
+ return messages.map((m, i) => {
136
+ if (i !== at || m.role !== "assistant")
137
+ return m;
138
+ const prev = hasKeys(m.providerOptions) ? m.providerOptions : {};
139
+ return { ...m, providerOptions: { ...prev, [USAGE_KEY]: stamp } };
140
+ });
141
+ }, extractModel = (content) => {
142
+ const model = content.find((p) => p.type === "finish")?.metadata?.router?.model;
143
+ return typeof model === "string" && model.length > 0 ? Option.some(model) : Option.none();
144
+ }, assistantUsage = (msg) => {
145
+ if (msg.role !== "assistant")
146
+ return Option.none();
147
+ const usage = msg.providerOptions?.[USAGE_KEY];
148
+ return hasKeys(usage) ? Option.some(usage) : Option.none();
149
+ }, assistantModel = (msg) => {
150
+ if (msg.role !== "assistant")
151
+ return Option.none();
152
+ const model = msg.providerOptions?.[USAGE_KEY]?.model;
153
+ return typeof model === "string" && model.length > 0 ? Option.some(model) : Option.none();
154
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { FiberRef, Option } from "effect";
2
+ export declare const CurrentModelCallPolicy: FiberRef.FiberRef<Option.Option<{
3
+ readonly effort: "none" | "low" | "medium" | "high" | "xhigh" | "max";
4
+ readonly maxOutputTokens?: number | undefined;
5
+ }>>;
6
+ /**
7
+ * Whether the CURRENT model call may legitimately return an empty response.
8
+ *
9
+ * An HTTP-200-but-empty body on a run's FIRST call is a provider-outage
10
+ * signature and must stay hard-rejected (it would fake-complete the turn).
11
+ * But once the run has executed a tool call, a following empty response is
12
+ * how models say "I'm done" — rejecting it sends the call into the patient
13
+ * outage ladder, which parks the turn until a deadline kills it (observed
14
+ * live: math turns riding 120s budgets, ui composers riding 55s soft
15
+ * deadlines, for work that finished in seconds).
16
+ *
17
+ * The LOOP sets this once the run has tool calls; adapters' empty-response
18
+ * rejection consults it at call time. A FiberRef, not a port: ambient call
19
+ * metadata, exactly like {@link CurrentModelCallPolicy}.
20
+ */
21
+ export declare const CurrentEmptyResponseTolerance: FiberRef.FiberRef<boolean>;
@@ -0,0 +1,2 @@
1
+ import { FiberRef, GlobalValue, Option } from "effect";
2
+ export const CurrentModelCallPolicy = GlobalValue.globalValue("@xandreed/core/CurrentModelCallPolicy", () => FiberRef.unsafeMake(Option.none())), CurrentEmptyResponseTolerance = GlobalValue.globalValue("@xandreed/core/CurrentEmptyResponseTolerance", () => FiberRef.unsafeMake(!1));
@@ -0,0 +1,8 @@
1
+ import { FiberRef, Option } from "effect";
2
+ export declare const CurrentPromptProvenance: FiberRef.FiberRef<Option.Option<{
3
+ readonly id: string & import("effect/Brand").Brand<"PromptId">;
4
+ readonly version: string;
5
+ readonly variant: string;
6
+ readonly modelOverride: Option.Option<string>;
7
+ readonly hash: string;
8
+ }>>;
@@ -0,0 +1,2 @@
1
+ import { FiberRef, GlobalValue, Option } from "effect";
2
+ export const CurrentPromptProvenance = GlobalValue.globalValue("@xandreed/core/CurrentPromptProvenance", () => FiberRef.unsafeMake(Option.none()));
@@ -0,0 +1,3 @@
1
+ import { FiberRef, Option } from "effect";
2
+ /** Correlation only; unset for work outside the agent loop. */
3
+ export declare const CurrentAgentStep: FiberRef.FiberRef<Option.Option<number>>;
@@ -0,0 +1,2 @@
1
+ import { FiberRef, GlobalValue, Option } from "effect";
2
+ export const CurrentAgentStep = GlobalValue.globalValue("@xandreed/core/CurrentAgentStep", () => FiberRef.unsafeMake(Option.none()));
@@ -0,0 +1,31 @@
1
+ import { Effect, Stream } from "effect";
2
+ /**
3
+ * Folds a `streamText` part stream into the SETTLED turn shape `step()`
4
+ * reads — after this fold the loop's turn body is identical code on both
5
+ * paths, so replay ≡ live for final events by construction.
6
+ *
7
+ * - Streamed text/reasoning chunks occupy ORDERED SLOTS at the position
8
+ * their start/first-delta arrived; deltas append in place (and fan out
9
+ * through `onDelta` for live rendering); a chunk that accumulated nothing
10
+ * is dropped (content-part identity).
11
+ * - Every settled part (tool-call, tool-result, finish, provider extras)
12
+ * passes through at its arrival position unchanged — metadata included,
13
+ * so the router's model stamp and the usage fold read exactly what the
14
+ * non-streaming path reads.
15
+ * - `finishReason`/`usage` lift off the finish part (the last one carrying
16
+ * usage wins, matching `extractUsage`'s scan).
17
+ */
18
+ export interface StreamDelta {
19
+ readonly channel: "text" | "reasoning" | "tool-params";
20
+ readonly id: string;
21
+ readonly delta: string;
22
+ /** The tool being called — set on `tool-params` deltas only. */
23
+ readonly toolName?: string;
24
+ }
25
+ /** Structurally what the loop reads off a `GenerateTextResponse`. */
26
+ export interface FoldedTurn {
27
+ readonly content: ReadonlyArray<unknown>;
28
+ readonly finishReason: string;
29
+ readonly usage: unknown;
30
+ }
31
+ export declare const foldStreamParts: <E, R, R2 = never>(parts: Stream.Stream<unknown, E, R>, onDelta: (delta: StreamDelta) => Effect.Effect<void, never, R2>) => Effect.Effect<FoldedTurn, E, R | R2>;
@@ -0,0 +1,58 @@
1
+ import { Effect, Option, Stream } from "effect";
2
+ const CHANNEL_BY_TYPE = {
3
+ "text-start": "text",
4
+ "text-delta": "text",
5
+ "text-end": "text",
6
+ "reasoning-start": "reasoning",
7
+ "reasoning-delta": "reasoning",
8
+ "reasoning-end": "reasoning"
9
+ }, openChunk = (entries, channel, id) => entries.some((e) => e.kind === "chunk" && e.channel === channel && e.id === id) ? entries : [...entries, { kind: "chunk", channel, id, text: "" }], appendDelta = (entries, channel, id, delta) => openChunk(entries, channel, id).map((e) => e.kind === "chunk" && e.channel === channel && e.id === id ? { ...e, text: e.text + delta } : e), entryParts = (entry) => {
10
+ if (entry.kind === "part")
11
+ return [entry.part];
12
+ return entry.text.length > 0 ? [{ type: entry.channel, text: entry.text }] : [];
13
+ };
14
+ export const foldStreamParts = (parts, onDelta) => Stream.runFoldEffect(parts, {
15
+ entries: [],
16
+ finishReason: Option.none(),
17
+ usage: Option.none()
18
+ }, (state, part) => {
19
+ const p = part, type = p.type ?? "";
20
+ if (type === "tool-params-start" || type === "tool-params-delta") {
21
+ const id = p.id ?? "tool-params-1";
22
+ return onDelta({
23
+ channel: "tool-params",
24
+ id,
25
+ delta: type === "tool-params-delta" ? p.delta ?? "" : "",
26
+ ...type === "tool-params-start" && p.name !== void 0 ? { toolName: p.name } : {}
27
+ }).pipe(Effect.as({
28
+ ...state,
29
+ entries: [...state.entries, { kind: "part", part }]
30
+ }));
31
+ }
32
+ const channel = CHANNEL_BY_TYPE[type];
33
+ if (channel !== void 0) {
34
+ const id = p.id ?? `${channel}-1`;
35
+ if (type.endsWith("-start"))
36
+ return Effect.succeed({ ...state, entries: openChunk(state.entries, channel, id) });
37
+ if (type.endsWith("-end"))
38
+ return Effect.succeed(state);
39
+ const delta = p.delta ?? "", next = { ...state, entries: appendDelta(state.entries, channel, id, delta) };
40
+ return delta.length > 0 ? onDelta({ channel, id, delta }).pipe(Effect.as(next)) : Effect.succeed(next);
41
+ }
42
+ if (type === "finish") {
43
+ const usage = p.usage, carriesUsage = usage?.inputTokens !== void 0 || usage?.outputTokens !== void 0;
44
+ return Effect.succeed({
45
+ entries: [...state.entries, { kind: "part", part }],
46
+ finishReason: Option.orElse(state.finishReason, () => Option.some(p.reason ?? "unknown")),
47
+ usage: carriesUsage || Option.isNone(state.usage) ? Option.some(p.usage) : state.usage
48
+ });
49
+ }
50
+ return Effect.succeed({
51
+ ...state,
52
+ entries: [...state.entries, { kind: "part", part }]
53
+ });
54
+ }).pipe(Effect.map((state) => ({
55
+ content: state.entries.flatMap(entryParts),
56
+ finishReason: Option.getOrElse(state.finishReason, () => "unknown"),
57
+ usage: Option.getOrUndefined(state.usage)
58
+ })));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { Tool, Toolkit } from "@effect/ai";
2
+ import { Context, Effect, Schema } from "effect";
3
+ import { McpClient } from "../ports/mcp-client.port.js";
4
+ import type { McpToolDescriptor } from "../ports/mcp-client.port.js";
5
+ export declare const McpDescribe: Tool.Tool<"mcp_describe", {
6
+ readonly parameters: Schema.Struct<{
7
+ server: Schema.SchemaClass<string, string, never>;
8
+ tool: Schema.SchemaClass<string, string, never>;
9
+ }>;
10
+ readonly success: Schema.Struct<{
11
+ server: typeof Schema.String;
12
+ tool: typeof Schema.String;
13
+ description: typeof Schema.String;
14
+ inputSchema: typeof Schema.Unknown;
15
+ }>;
16
+ readonly failure: Schema.Struct<{
17
+ error: typeof Schema.String;
18
+ message: typeof Schema.String;
19
+ }>;
20
+ readonly failureMode: "return";
21
+ }, never>;
22
+ export declare const McpCall: Tool.Tool<"mcp_call", {
23
+ readonly parameters: Schema.Struct<{
24
+ server: Schema.SchemaClass<string, string, never>;
25
+ tool: Schema.SchemaClass<string, string, never>;
26
+ args: Schema.Record$<typeof Schema.String, typeof Schema.Unknown>;
27
+ }>;
28
+ readonly success: typeof Schema.Unknown;
29
+ readonly failure: Schema.Struct<{
30
+ error: typeof Schema.String;
31
+ message: typeof Schema.String;
32
+ }>;
33
+ readonly failureMode: "return";
34
+ }, never>;
35
+ /** The bridge's toolkit is the fixed describe/call pair, but typed loosely
36
+ * (`Tool.Any`) so the populated toolkit and the empty one share one shape. */
37
+ export type McpBridgedTool = Tool.Any;
38
+ export interface McpBridge {
39
+ readonly toolkit: Toolkit.Toolkit<Record<string, McpBridgedTool>>;
40
+ /** The two tools' handlers, closed over the client + descriptor snapshot. */
41
+ readonly handlers: Context.Context<Tool.Handler<string>>;
42
+ /** Every configured tool, for the tier-1 prompt listing (names only). */
43
+ readonly descriptors: ReadonlyArray<McpToolDescriptor>;
44
+ }
45
+ export declare const emptyMcpBridge: McpBridge;
46
+ /**
47
+ * Snapshot the configured servers' tools into the fixed describe/call toolkit
48
+ * (taken once per run — no `tools/list_changed` handling in v1). No servers,
49
+ * or no tools across them, yields the empty bridge (no tool cost at all).
50
+ */
51
+ export declare const buildMcpBridge: Effect.Effect<McpBridge, never, McpClient>;
@@ -0,0 +1,70 @@
1
+ import { Tool, Toolkit } from "@effect/ai";
2
+ import { Context, Effect, Option, Schema } from "effect";
3
+ import { Failure } from "../domain/failure.entity.js";
4
+ import { McpClient } from "../ports/mcp-client.port.js";
5
+ const OUTPUT_CAP_CHARS = 16000, McpArgs = Schema.Record({ key: Schema.String, value: Schema.Unknown }), clipResult = (value) => {
6
+ const text = typeof value === "string" ? value : JSON.stringify(value) ?? "";
7
+ return text.length <= OUTPUT_CAP_CHARS ? value : `${text.slice(0, OUTPUT_CAP_CHARS)}
8
+ [\u2026clipped ${text.length - OUTPUT_CAP_CHARS} chars\u2026]`;
9
+ };
10
+ export const McpDescribe = Tool.make("mcp_describe", {
11
+ description: "Reveal ONE external MCP tool's parameters. Pass the server + tool exactly as listed under 'External MCP tools'. Returns {server, tool, description, inputSchema} \u2014 the JSON Schema of the arguments mcp_call expects. Call this BEFORE mcp_call the first time you use a tool.",
12
+ parameters: {
13
+ server: Schema.String.annotations({ description: "The server name, as listed." }),
14
+ tool: Schema.String.annotations({ description: "The tool name, as listed." })
15
+ },
16
+ success: Schema.Struct({
17
+ server: Schema.String,
18
+ tool: Schema.String,
19
+ description: Schema.String,
20
+ inputSchema: Schema.Unknown
21
+ }),
22
+ failure: Failure,
23
+ failureMode: "return"
24
+ }), McpCall = Tool.make("mcp_call", {
25
+ description: "Invoke an external MCP tool. Pass server + tool (as listed) and args as a JSON object matching the schema mcp_describe returned. A tool-level failure comes back as data (read it and adapt), not a dead turn. Returns the tool's result (clipped past 16k chars).",
26
+ parameters: {
27
+ server: Schema.String.annotations({ description: "The server name, as listed." }),
28
+ tool: Schema.String.annotations({ description: "The tool name, as listed." }),
29
+ args: McpArgs.annotations({
30
+ description: "The tool's arguments as a JSON object (see mcp_describe); {} if it takes none."
31
+ })
32
+ },
33
+ success: Schema.Unknown,
34
+ failure: Failure,
35
+ failureMode: "return"
36
+ }), emptyMcpBridge = {
37
+ toolkit: Toolkit.make(),
38
+ handlers: Context.empty(),
39
+ descriptors: []
40
+ }, buildMcpBridge = Effect.gen(function* () {
41
+ const client = yield* McpClient, descriptors = yield* client.listTools;
42
+ if (descriptors.length === 0)
43
+ return emptyMcpBridge;
44
+ const find = (server, tool) => Option.fromNullable(descriptors.find((d) => d.server === server && d.name === tool)), tools = [McpDescribe, McpCall], toolkit = Toolkit.make(...tools), handlerRecord = {
45
+ mcp_describe: (params) => Option.match(find(params.server, params.tool), {
46
+ onNone: () => Effect.fail({
47
+ error: "UnknownMcpTool",
48
+ message: `no MCP tool "${params.tool}" on server "${params.server}" \u2014 use one listed under 'External MCP tools'`
49
+ }),
50
+ onSome: (descriptor) => Effect.succeed({
51
+ server: descriptor.server,
52
+ tool: descriptor.name,
53
+ description: Option.getOrElse(descriptor.description, () => ""),
54
+ inputSchema: descriptor.inputSchema
55
+ })
56
+ }),
57
+ mcp_call: (params) => Option.isNone(find(params.server, params.tool)) ? Effect.fail({
58
+ error: "UnknownMcpTool",
59
+ message: `no MCP tool "${params.tool}" on server "${params.server}" \u2014 use one listed under 'External MCP tools'`
60
+ }) : client.callTool(params.server, params.tool, params.args).pipe(Effect.flatMap((outcome) => outcome.isError ? Effect.fail({
61
+ error: "McpToolError",
62
+ message: typeof outcome.result === "string" ? outcome.result.slice(0, 2000) : JSON.stringify(outcome.result).slice(0, 2000)
63
+ }) : Effect.succeed(clipResult(outcome.result))), Effect.catchTag("McpError", (error) => Effect.fail({ error: "McpTransportError", message: error.message })))
64
+ }, handlers = yield* toolkit.toContext(handlerRecord);
65
+ return {
66
+ toolkit,
67
+ handlers,
68
+ descriptors
69
+ };
70
+ }).pipe(Effect.catchAllCause((cause) => Effect.logWarning(`MCP bridge unavailable \u2014 continuing without MCP tools: ${cause}`).pipe(Effect.as(emptyMcpBridge))));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,50 @@
1
+ import { Context, Option, Schema } from "effect";
2
+ import type { Effect, Redacted } from "effect";
3
+ import type { ProviderId } from "../domain/model-selection.entity.js";
4
+ /**
5
+ * A stored provider credential: a raw API key, an OAuth subscription (with
6
+ * refresh material and absolute expiry, epoch ms), or a local provider that
7
+ * needs no key. Matches the on-disk `auth.json` vocabulary the previous line
8
+ * established, so existing credentials keep working.
9
+ */
10
+ export declare const Credential: Schema.Union<[Schema.Struct<{
11
+ type: Schema.Literal<["api_key"]>;
12
+ key: typeof Schema.String;
13
+ }>, Schema.Struct<{
14
+ type: Schema.Literal<["oauth"]>;
15
+ access: typeof Schema.String;
16
+ refresh: typeof Schema.String;
17
+ expires: typeof Schema.Number;
18
+ accountId: Schema.optional<typeof Schema.String>;
19
+ installationId: Schema.optional<typeof Schema.String>;
20
+ }>, Schema.Struct<{
21
+ type: Schema.Literal<["local"]>;
22
+ baseUrl: Schema.optional<typeof Schema.String>;
23
+ }>]>;
24
+ export type Credential = typeof Credential.Type;
25
+ declare const AuthError_base: Schema.TaggedErrorClass<AuthError, "AuthError", {
26
+ readonly _tag: Schema.tag<"AuthError">;
27
+ } & {
28
+ provider: typeof Schema.String;
29
+ message: typeof Schema.String;
30
+ }>;
31
+ export declare class AuthError extends AuthError_base {
32
+ }
33
+ declare const AuthStore_base: Context.TagClass<AuthStore, "@xandreed/core/AuthStore", {
34
+ readonly all: Effect.Effect<ReadonlyMap<string, Credential>, AuthError>;
35
+ readonly get: (p: ProviderId) => Effect.Effect<Option.Option<Credential>, AuthError>;
36
+ readonly resolveKey: (p: ProviderId) => Effect.Effect<Option.Option<Redacted.Redacted<string>>, AuthError>;
37
+ /** Persist a credential (the human's `:login` action). */
38
+ readonly set: (p: ProviderId, credential: Credential) => Effect.Effect<void, AuthError>;
39
+ /** Remove a provider's credential everywhere it is stored (`:logout`). */
40
+ readonly remove: (p: ProviderId) => Effect.Effect<void, AuthError>;
41
+ }>;
42
+ /**
43
+ * The credential store, read lazily per request: `resolveKey` returns a
44
+ * usable secret (refreshing + persisting a near-expiry OAuth token first),
45
+ * so a credential added mid-session takes effect on the next call with no
46
+ * restart. `None` means the provider is not configured.
47
+ */
48
+ export declare class AuthStore extends AuthStore_base {
49
+ }
50
+ export {};
@@ -0,0 +1,18 @@
1
+ import { Context, Option, Schema } from "effect";
2
+ export const Credential = Schema.Union(Schema.Struct({ type: Schema.Literal("api_key"), key: Schema.String }), Schema.Struct({
3
+ type: Schema.Literal("oauth"),
4
+ access: Schema.String,
5
+ refresh: Schema.String,
6
+ expires: Schema.Number,
7
+ accountId: Schema.optional(Schema.String),
8
+ installationId: Schema.optional(Schema.String)
9
+ }), Schema.Struct({ type: Schema.Literal("local"), baseUrl: Schema.optional(Schema.String) }));
10
+
11
+ export class AuthError extends Schema.TaggedError()("AuthError", {
12
+ provider: Schema.String,
13
+ message: Schema.String
14
+ }) {
15
+ }
16
+
17
+ export class AuthStore extends Context.Tag("@xandreed/core/AuthStore")() {
18
+ }
@@ -0,0 +1,15 @@
1
+ import { Context } from "effect";
2
+ import type { Effect } from "effect";
3
+ import type { CapabilityCatalog, CapabilitySelection } from "../harness/capability.entity.js";
4
+ import type { HarnessError } from "../harness/plugin.entity.js";
5
+ declare const Capabilities_base: Context.TagClass<Capabilities, "efferent/Capabilities", {
6
+ readonly catalog: CapabilityCatalog;
7
+ }>;
8
+ export declare class Capabilities extends Capabilities_base {
9
+ }
10
+ declare const IntentMatcher_base: Context.TagClass<IntentMatcher, "efferent/IntentMatcher", {
11
+ readonly match: (message: string, catalog: CapabilityCatalog) => Effect.Effect<CapabilitySelection, HarnessError>;
12
+ }>;
13
+ export declare class IntentMatcher extends IntentMatcher_base {
14
+ }
15
+ export {};
@@ -0,0 +1,7 @@
1
+ import { Context } from "effect";
2
+
3
+ export class Capabilities extends Context.Tag("efferent/Capabilities")() {
4
+ }
5
+
6
+ export class IntentMatcher extends Context.Tag("efferent/IntentMatcher")() {
7
+ }