@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.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/dist/domain/agent-failure.entity.d.ts +12 -0
- package/dist/domain/agent-failure.entity.functions.d.ts +5 -0
- package/dist/domain/agent-failure.entity.functions.js +8 -0
- package/dist/domain/agent-failure.entity.js +8 -0
- package/dist/domain/failure.entity.d.ts +12 -0
- package/dist/domain/failure.entity.functions.d.ts +3 -0
- package/dist/domain/failure.entity.functions.js +1 -0
- package/dist/domain/failure.entity.js +5 -0
- package/dist/domain/loop-event.entity.d.ts +94 -0
- package/dist/domain/loop-event.entity.js +0 -0
- package/dist/domain/message.entity.d.ts +227 -0
- package/dist/domain/message.entity.js +51 -0
- package/dist/domain/model-call-policy.entity.d.ts +12 -0
- package/dist/domain/model-call-policy.entity.js +5 -0
- package/dist/domain/model-catalog.entity.d.ts +10 -0
- package/dist/domain/model-catalog.entity.js +7 -0
- package/dist/domain/model-selection.entity.d.ts +24 -0
- package/dist/domain/model-selection.entity.functions.d.ts +5 -0
- package/dist/domain/model-selection.entity.functions.js +9 -0
- package/dist/domain/model-selection.entity.js +8 -0
- package/dist/domain/prompt-provenance.entity.d.ts +11 -0
- package/dist/domain/prompt-provenance.entity.js +8 -0
- package/dist/domain/token-usage.entity.d.ts +9 -0
- package/dist/domain/token-usage.entity.functions.d.ts +3 -0
- package/dist/domain/token-usage.entity.functions.js +11 -0
- package/dist/domain/token-usage.entity.js +7 -0
- package/dist/harness/capability.entity.d.ts +61 -0
- package/dist/harness/capability.entity.functions.d.ts +6 -0
- package/dist/harness/capability.entity.functions.js +10 -0
- package/dist/harness/capability.entity.js +26 -0
- package/dist/harness/capability.test.d.ts +1 -0
- package/dist/harness/config.entity.d.ts +48 -0
- package/dist/harness/config.entity.functions.d.ts +3 -0
- package/dist/harness/config.entity.functions.js +1 -0
- package/dist/harness/config.entity.js +18 -0
- package/dist/harness/plugin.adapter.d.ts +18 -0
- package/dist/harness/plugin.adapter.js +13 -0
- package/dist/harness/plugin.entity.d.ts +25 -0
- package/dist/harness/plugin.entity.js +9 -0
- package/dist/harness/session.entity.d.ts +33 -0
- package/dist/harness/session.entity.js +25 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +99 -0
- package/dist/loop/cacheKey.d.ts +13 -0
- package/dist/loop/cacheKey.js +2 -0
- package/dist/loop/mapping.d.ts +79 -0
- package/dist/loop/mapping.js +154 -0
- package/dist/loop/mapping.test.d.ts +1 -0
- package/dist/loop/modelPolicy.d.ts +21 -0
- package/dist/loop/modelPolicy.js +2 -0
- package/dist/loop/promptProvenance.d.ts +8 -0
- package/dist/loop/promptProvenance.js +2 -0
- package/dist/loop/stepContext.d.ts +3 -0
- package/dist/loop/stepContext.js +2 -0
- package/dist/loop/streamFold.d.ts +31 -0
- package/dist/loop/streamFold.js +58 -0
- package/dist/loop/streamFold.test.d.ts +1 -0
- package/dist/mcp/bridge.d.ts +51 -0
- package/dist/mcp/bridge.js +70 -0
- package/dist/mcp/bridge.test.d.ts +1 -0
- package/dist/ports/auth-store.port.d.ts +50 -0
- package/dist/ports/auth-store.port.js +18 -0
- package/dist/ports/capability.port.d.ts +15 -0
- package/dist/ports/capability.port.js +7 -0
- package/dist/ports/conversation-store.port.d.ts +250 -0
- package/dist/ports/conversation-store.port.js +36 -0
- package/dist/ports/file-system.port.d.ts +26 -0
- package/dist/ports/file-system.port.js +10 -0
- package/dist/ports/harness.port.d.ts +106 -0
- package/dist/ports/harness.port.js +31 -0
- package/dist/ports/mcp-client.port.d.ts +74 -0
- package/dist/ports/mcp-client.port.js +24 -0
- package/dist/ports/model-catalog.port.d.ts +10 -0
- package/dist/ports/model-catalog.port.js +4 -0
- package/dist/ports/settings-store.port.d.ts +85 -0
- package/dist/ports/settings-store.port.js +36 -0
- package/dist/ports/shell.port.d.ts +44 -0
- package/dist/ports/shell.port.js +16 -0
- package/dist/ports/utility-llm.port.d.ts +48 -0
- package/dist/ports/utility-llm.port.js +16 -0
- package/dist/session/chassis.d.ts +59 -0
- package/dist/session/chassis.js +27 -0
- package/dist/session/chassis.test.d.ts +1 -0
- package/dist/spec/SpecDoc.d.ts +162 -0
- package/dist/spec/SpecDoc.js +59 -0
- package/dist/spec/codec.d.ts +30 -0
- package/dist/spec/codec.js +123 -0
- package/dist/spec/codec.test.d.ts +1 -0
- package/dist/spec/frontmatter.d.ts +16 -0
- package/dist/spec/frontmatter.js +25 -0
- package/dist/util/json.d.ts +19 -0
- package/dist/util/json.js +15 -0
- 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,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 {};
|