reflex-state 0.1.0-alpha.1
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 +255 -0
- package/README_ja.md +253 -0
- package/dist/cli_io.d.ts +8 -0
- package/dist/cli_io.js +45 -0
- package/dist/composition.d.ts +3 -0
- package/dist/composition.js +8 -0
- package/dist/core/config.d.ts +37 -0
- package/dist/core/config.js +27 -0
- package/dist/core/config_validation.d.ts +6 -0
- package/dist/core/config_validation.js +109 -0
- package/dist/core/engine.d.ts +28 -0
- package/dist/core/engine.js +72 -0
- package/dist/core/events.d.ts +7 -0
- package/dist/core/events.js +56 -0
- package/dist/core/extraction.d.ts +10 -0
- package/dist/core/extraction.js +119 -0
- package/dist/core/metrics.d.ts +52 -0
- package/dist/core/metrics.js +100 -0
- package/dist/core/reducer.d.ts +18 -0
- package/dist/core/reducer.js +206 -0
- package/dist/core/serialization.d.ts +11 -0
- package/dist/core/serialization.js +160 -0
- package/dist/core/state_view.d.ts +27 -0
- package/dist/core/state_view.js +28 -0
- package/dist/core/types.d.ts +188 -0
- package/dist/core/types.js +1 -0
- package/dist/core/updater.d.ts +31 -0
- package/dist/core/updater.js +27 -0
- package/dist/core/verification.d.ts +20 -0
- package/dist/core/verification.js +234 -0
- package/dist/export_trace_cli.d.ts +2 -0
- package/dist/export_trace_cli.js +45 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +8 -0
- package/dist/pi/commands.d.ts +3 -0
- package/dist/pi/commands.js +123 -0
- package/dist/pi/configuration.d.ts +12 -0
- package/dist/pi/configuration.js +48 -0
- package/dist/pi/extension.d.ts +3 -0
- package/dist/pi/extension.js +113 -0
- package/dist/pi/index.d.ts +2 -0
- package/dist/pi/index.js +5 -0
- package/dist/pi/normalization.d.ts +31 -0
- package/dist/pi/normalization.js +72 -0
- package/dist/pi/persistence.d.ts +15 -0
- package/dist/pi/persistence.js +65 -0
- package/dist/pi/projection.d.ts +11 -0
- package/dist/pi/projection.js +159 -0
- package/dist/pi/runtime.d.ts +35 -0
- package/dist/pi/runtime.js +139 -0
- package/dist/pi/state_block.d.ts +10 -0
- package/dist/pi/state_block.js +123 -0
- package/dist/pi/trace.d.ts +14 -0
- package/dist/pi/trace.js +106 -0
- package/dist/replay/runner.d.ts +50 -0
- package/dist/replay/runner.js +21 -0
- package/dist/replay/trace.d.ts +2 -0
- package/dist/replay/trace.js +7 -0
- package/dist/replay_cli.d.ts +2 -0
- package/dist/replay_cli.js +71 -0
- package/dist/typesafe/client.d.ts +21 -0
- package/dist/typesafe/client.js +49 -0
- package/dist/typesafe/deadline.d.ts +8 -0
- package/dist/typesafe/deadline.js +35 -0
- package/dist/typesafe/decisions.d.ts +5 -0
- package/dist/typesafe/decisions.js +94 -0
- package/dist/typesafe/gating.d.ts +7 -0
- package/dist/typesafe/gating.js +48 -0
- package/dist/typesafe/input.d.ts +3 -0
- package/dist/typesafe/input.js +115 -0
- package/dist/typesafe/questions.d.ts +5 -0
- package/dist/typesafe/questions.js +50 -0
- package/dist/typesafe/request_plan.d.ts +19 -0
- package/dist/typesafe/request_plan.js +96 -0
- package/dist/typesafe/updater.d.ts +17 -0
- package/dist/typesafe/updater.js +82 -0
- package/package.json +97 -0
package/dist/pi/trace.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { textContent } from "../core/events.js";
|
|
2
|
+
import { isRecord } from "../core/serialization.js";
|
|
3
|
+
import { PiEventNormalizer } from "./normalization.js";
|
|
4
|
+
import { reconstruct } from "./persistence.js";
|
|
5
|
+
export function exportSession(entries, options) {
|
|
6
|
+
const branch = selectBranch(entries, options.leaf);
|
|
7
|
+
const stored = branch.map((entry) => ({
|
|
8
|
+
type: String(entry.type),
|
|
9
|
+
...(typeof entry.customType === "string" ? { customType: entry.customType } : {}),
|
|
10
|
+
data: entry.data,
|
|
11
|
+
}));
|
|
12
|
+
const restored = reconstruct(stored);
|
|
13
|
+
const hasRecords = stored.some((entry) => entry.customType === "reflex-state.transition" || entry.customType === "reflex-state.reset");
|
|
14
|
+
const header = entries.find((entry) => entry.type === "session");
|
|
15
|
+
return {
|
|
16
|
+
events: hasRecords && !restored.legacy
|
|
17
|
+
? restored.transitions.map((record) => record.event)
|
|
18
|
+
: deriveEvents(branch, options.config),
|
|
19
|
+
transitions: restored.transitions,
|
|
20
|
+
cwd: typeof header?.cwd === "string" ? header.cwd : ".",
|
|
21
|
+
config: restored.transitions[0]?.config ?? options.config,
|
|
22
|
+
leaf: branch.at(-1)?.id ?? null,
|
|
23
|
+
formatVersion: restored.legacy ? 1 : 2,
|
|
24
|
+
legacy: restored.legacy,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function selectBranch(entries, leaf) {
|
|
28
|
+
const indexed = entries.filter((entry) => typeof entry.id === "string" && entry.type !== "session");
|
|
29
|
+
const byId = new Map(indexed.map((entry) => [entry.id, entry]));
|
|
30
|
+
if (byId.size !== indexed.length)
|
|
31
|
+
throw new Error("Duplicate Pi session entry ID");
|
|
32
|
+
let id = leaf ?? indexed.at(-1)?.id;
|
|
33
|
+
const branch = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
while (typeof id === "string") {
|
|
36
|
+
const entry = byId.get(id);
|
|
37
|
+
if (!entry || seen.has(id))
|
|
38
|
+
throw new Error("Missing or cyclic Pi branch entry");
|
|
39
|
+
seen.add(id);
|
|
40
|
+
branch.push(entry);
|
|
41
|
+
id = entry.parentId;
|
|
42
|
+
}
|
|
43
|
+
return branch.reverse();
|
|
44
|
+
}
|
|
45
|
+
function deriveEvents(branch, config) {
|
|
46
|
+
let timestamp = 0;
|
|
47
|
+
const normalizer = new PiEventNormalizer({
|
|
48
|
+
eventCount: 0,
|
|
49
|
+
turnIndex: 0,
|
|
50
|
+
config,
|
|
51
|
+
now: () => timestamp,
|
|
52
|
+
});
|
|
53
|
+
const events = [];
|
|
54
|
+
for (const entry of branch) {
|
|
55
|
+
if (entry.type !== "message" || !isRecord(entry.message))
|
|
56
|
+
continue;
|
|
57
|
+
const message = entry.message;
|
|
58
|
+
timestamp =
|
|
59
|
+
typeof message.timestamp === "number"
|
|
60
|
+
? message.timestamp
|
|
61
|
+
: Date.parse(String(entry.timestamp));
|
|
62
|
+
if (!Number.isFinite(timestamp))
|
|
63
|
+
throw new Error("Invalid Pi message timestamp");
|
|
64
|
+
if (message.role === "user")
|
|
65
|
+
events.push(normalizer.prompt(textContent(message.content), timestamp));
|
|
66
|
+
if (message.role === "toolResult")
|
|
67
|
+
events.push(normalizer.result({
|
|
68
|
+
type: "tool_result",
|
|
69
|
+
toolCallId: string(message.toolCallId),
|
|
70
|
+
toolName: string(message.toolName),
|
|
71
|
+
input: {},
|
|
72
|
+
content: [{ type: "text", text: textContent(message.content) }],
|
|
73
|
+
isError: message.isError === true,
|
|
74
|
+
details: undefined,
|
|
75
|
+
}));
|
|
76
|
+
if (message.role === "assistant")
|
|
77
|
+
events.push(...assistantEvents(message, normalizer));
|
|
78
|
+
}
|
|
79
|
+
return events;
|
|
80
|
+
}
|
|
81
|
+
function assistantEvents(message, normalizer) {
|
|
82
|
+
const events = [];
|
|
83
|
+
if (!Array.isArray(message.content))
|
|
84
|
+
throw new Error("Invalid assistant content");
|
|
85
|
+
for (const block of message.content) {
|
|
86
|
+
if (!isRecord(block) || block.type !== "toolCall")
|
|
87
|
+
continue;
|
|
88
|
+
if (!isRecord(block.arguments))
|
|
89
|
+
throw new Error("Invalid tool arguments");
|
|
90
|
+
events.push(normalizer.call({
|
|
91
|
+
type: "tool_call",
|
|
92
|
+
toolCallId: string(block.id),
|
|
93
|
+
toolName: string(block.name),
|
|
94
|
+
input: block.arguments,
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
if (["stop", "length", "error", "aborted"].includes(String(message.stopReason))) {
|
|
98
|
+
events.push(normalizer.end([message]));
|
|
99
|
+
}
|
|
100
|
+
return events;
|
|
101
|
+
}
|
|
102
|
+
function string(value) {
|
|
103
|
+
if (typeof value !== "string")
|
|
104
|
+
throw new Error("Invalid Pi tool reference");
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ReflexStateConfig } from "../core/config.js";
|
|
2
|
+
import type { AgentEvent, StateTransitionRecord } from "../core/types.js";
|
|
3
|
+
import type { StateUpdater } from "../core/updater.js";
|
|
4
|
+
interface ReplayOptions {
|
|
5
|
+
readonly cwd: string;
|
|
6
|
+
readonly config: ReflexStateConfig;
|
|
7
|
+
readonly updater: StateUpdater;
|
|
8
|
+
readonly recording?: readonly StateTransitionRecord[];
|
|
9
|
+
}
|
|
10
|
+
export declare function replay(events: readonly AgentEvent[], options: ReplayOptions): Promise<{
|
|
11
|
+
state: import("../core/types.js").HotState;
|
|
12
|
+
transitions: StateTransitionRecord[];
|
|
13
|
+
metrics: {
|
|
14
|
+
events: number;
|
|
15
|
+
transitions: number;
|
|
16
|
+
jevCalls: number;
|
|
17
|
+
jevFailures: Record<string, number>;
|
|
18
|
+
questions: Record<string, number>;
|
|
19
|
+
applied: number;
|
|
20
|
+
uncertain: number;
|
|
21
|
+
shadowAgreement: number | undefined;
|
|
22
|
+
jevLatencyMs: {
|
|
23
|
+
mean: number | undefined;
|
|
24
|
+
p50: number | undefined;
|
|
25
|
+
p95: number | undefined;
|
|
26
|
+
};
|
|
27
|
+
jevInputTokens: number | undefined;
|
|
28
|
+
jevOutputTokens: number | undefined;
|
|
29
|
+
projection: {
|
|
30
|
+
calls: number;
|
|
31
|
+
last: import("../core/types.js").ProjectionMeasurement | undefined;
|
|
32
|
+
modes: Record<string, number>;
|
|
33
|
+
meanMessagesBefore: number | undefined;
|
|
34
|
+
meanMessagesAfter: number | undefined;
|
|
35
|
+
meanMessagesOmitted: number | undefined;
|
|
36
|
+
meanCharsBefore: number | undefined;
|
|
37
|
+
meanCharsAfter: number | undefined;
|
|
38
|
+
meanStateBlockChars: number | undefined;
|
|
39
|
+
fallbacks: Record<string, number>;
|
|
40
|
+
};
|
|
41
|
+
provider: {
|
|
42
|
+
calls: number;
|
|
43
|
+
input: number | undefined;
|
|
44
|
+
cacheRead: number | undefined;
|
|
45
|
+
cacheWrite: number | undefined;
|
|
46
|
+
output: number | undefined;
|
|
47
|
+
} | undefined;
|
|
48
|
+
};
|
|
49
|
+
}>;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StateEngine } from "../core/engine.js";
|
|
2
|
+
import { Metrics } from "../core/metrics.js";
|
|
3
|
+
export async function replay(events, options) {
|
|
4
|
+
const metrics = new Metrics();
|
|
5
|
+
const engine = new StateEngine({
|
|
6
|
+
...options,
|
|
7
|
+
onTransition: (record) => metrics.transition(record),
|
|
8
|
+
});
|
|
9
|
+
const recording = new Map(options.recording?.map((record) => [record.event.id, record]));
|
|
10
|
+
const transitions = [];
|
|
11
|
+
for (const event of events) {
|
|
12
|
+
const record = recording.get(event.id);
|
|
13
|
+
if (record) {
|
|
14
|
+
if (record.after.version !== 2)
|
|
15
|
+
throw new Error("Unsupported legacy recorded replay");
|
|
16
|
+
await engine.configure(record.config, options.updater, record.cwd);
|
|
17
|
+
}
|
|
18
|
+
transitions.push(await engine.process(event));
|
|
19
|
+
}
|
|
20
|
+
return { state: engine.state, transitions, metrics: metrics.snapshot() };
|
|
21
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { jsonLines, readConfigFile, traceMetadata, writeArtifacts } from "./cli_io.js";
|
|
6
|
+
import { createUpdater } from "./composition.js";
|
|
7
|
+
import { NoopStateUpdater, RecordedDecisionsUpdater } from "./core/updater.js";
|
|
8
|
+
import { replay } from "./replay/runner.js";
|
|
9
|
+
import { parseEvents, parseRecording } from "./replay/trace.js";
|
|
10
|
+
import { redact } from "./typesafe/input.js";
|
|
11
|
+
async function main() {
|
|
12
|
+
const { values, positionals } = parseArgs({
|
|
13
|
+
allowPositionals: true,
|
|
14
|
+
options: {
|
|
15
|
+
updater: { type: "string", default: "noop" },
|
|
16
|
+
config: { type: "string" },
|
|
17
|
+
cwd: { type: "string" },
|
|
18
|
+
out: { type: "string", default: "replay-output" },
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
const input = positionals[0];
|
|
22
|
+
if (!input || positionals.length !== 1)
|
|
23
|
+
throw new Error("Usage: reflex-state-replay <events.jsonl> --updater noop|jev|recorded[:path] [--config file] [--cwd dir] [--out dir]");
|
|
24
|
+
const mode = values.updater;
|
|
25
|
+
if (mode !== "noop" && mode !== "jev" && mode !== "recorded" && !mode.startsWith("recorded:"))
|
|
26
|
+
throw new Error("Unknown updater");
|
|
27
|
+
const recordedPath = mode.startsWith("recorded")
|
|
28
|
+
? mode.slice(9) || join(dirname(input), "transitions.jsonl")
|
|
29
|
+
: undefined;
|
|
30
|
+
const recording = recordedPath ? parseRecording(await readFile(recordedPath, "utf8")) : undefined;
|
|
31
|
+
const metadata = await traceMetadata(join(dirname(input), "trace_meta.json"));
|
|
32
|
+
const base = values.config
|
|
33
|
+
? await readConfigFile(values.config)
|
|
34
|
+
: (metadata.config ?? (await readConfigFile(undefined)));
|
|
35
|
+
const config = {
|
|
36
|
+
...base,
|
|
37
|
+
jev: {
|
|
38
|
+
...base.jev,
|
|
39
|
+
enabled: process.env.REFLEX_STATE_DISABLE_JEV === "1" ? false : base.jev.enabled,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const updater = recording
|
|
43
|
+
? new RecordedDecisionsUpdater(recording)
|
|
44
|
+
: mode === "jev"
|
|
45
|
+
? createUpdater(config, console.error)
|
|
46
|
+
: new NoopStateUpdater();
|
|
47
|
+
const result = await replay(parseEvents(await readFile(input, "utf8")), {
|
|
48
|
+
cwd: values.cwd ?? metadata.cwd ?? process.cwd(),
|
|
49
|
+
config,
|
|
50
|
+
updater,
|
|
51
|
+
...(recording ? { recording } : {}),
|
|
52
|
+
});
|
|
53
|
+
const summary = result.transitions.length +
|
|
54
|
+
" events; task " +
|
|
55
|
+
result.state.taskStatus +
|
|
56
|
+
"; Jev calls " +
|
|
57
|
+
result.metrics.jevCalls +
|
|
58
|
+
".";
|
|
59
|
+
await writeArtifacts(values.out, {
|
|
60
|
+
"final_state.json": JSON.stringify(result.state, null, 2) + "\n",
|
|
61
|
+
"transitions.jsonl": jsonLines(result.transitions),
|
|
62
|
+
"metrics.json": JSON.stringify(result.metrics, null, 2) + "\n",
|
|
63
|
+
"summary.txt": summary + "\n",
|
|
64
|
+
}, [input, ...(recordedPath ? [recordedPath] : [])]);
|
|
65
|
+
console.log(JSON.stringify(result.state, null, 2));
|
|
66
|
+
console.error(summary + " Artifacts: " + values.out);
|
|
67
|
+
}
|
|
68
|
+
await main().catch((error) => {
|
|
69
|
+
console.error(redact(error instanceof Error ? error.message : "Replay failed"));
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReflexStateConfig } from "../core/config.js";
|
|
2
|
+
export type Question = {
|
|
3
|
+
readonly type: "noul";
|
|
4
|
+
readonly instructions: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly type: "choice";
|
|
7
|
+
readonly instructions: string;
|
|
8
|
+
readonly criteria: Record<string, string>;
|
|
9
|
+
};
|
|
10
|
+
export interface SystemOneRequest {
|
|
11
|
+
readonly state: string;
|
|
12
|
+
readonly questions: Record<string, Question>;
|
|
13
|
+
readonly model: string;
|
|
14
|
+
}
|
|
15
|
+
export interface TypeSafeSystemOneClient {
|
|
16
|
+
systemOne(request: SystemOneRequest, options: {
|
|
17
|
+
signal: AbortSignal;
|
|
18
|
+
}): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
export declare function failureKind(error: unknown): string;
|
|
21
|
+
export declare function createTypeSafeClient(config: ReflexStateConfig): TypeSafeSystemOneClient;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { TypeSafeClient, AuthenticationError, RateLimitError, APITimeoutError, APIConnectionError, BadRequestError, UnprocessableEntityError, InternalServerError, TypeSafeError, } from "@typesafe-ai/sdk";
|
|
2
|
+
import { InvalidResponseError } from "./gating.js";
|
|
3
|
+
export function failureKind(error) {
|
|
4
|
+
if (error instanceof AuthenticationError)
|
|
5
|
+
return "auth_error";
|
|
6
|
+
if (error instanceof APITimeoutError)
|
|
7
|
+
return "timeout";
|
|
8
|
+
if (error instanceof RateLimitError)
|
|
9
|
+
return "rate_limit";
|
|
10
|
+
if (error instanceof APIConnectionError)
|
|
11
|
+
return "connection_error";
|
|
12
|
+
if (error instanceof BadRequestError)
|
|
13
|
+
return "bad_request";
|
|
14
|
+
if (error instanceof UnprocessableEntityError)
|
|
15
|
+
return "unprocessable";
|
|
16
|
+
if (error instanceof InternalServerError)
|
|
17
|
+
return "server_error";
|
|
18
|
+
if (error instanceof InvalidResponseError)
|
|
19
|
+
return "invalid_response";
|
|
20
|
+
if (error instanceof Error && /api[_ ]?key/i.test(error.message))
|
|
21
|
+
return "auth_error";
|
|
22
|
+
if (error instanceof TypeSafeError)
|
|
23
|
+
return "sdk_error";
|
|
24
|
+
return "updater_error";
|
|
25
|
+
}
|
|
26
|
+
export function createTypeSafeClient(config) {
|
|
27
|
+
let client;
|
|
28
|
+
return {
|
|
29
|
+
systemOne(request, options) {
|
|
30
|
+
client ??= new TypeSafeClient({
|
|
31
|
+
defaultModel: config.jev.model,
|
|
32
|
+
timeout: config.jev.timeoutMs,
|
|
33
|
+
retry: { maxRetries: config.jev.maxRetries },
|
|
34
|
+
logLevel: "warn",
|
|
35
|
+
logger: {
|
|
36
|
+
debug() { },
|
|
37
|
+
info() { },
|
|
38
|
+
warn() {
|
|
39
|
+
console.warn("TypeSafe SDK warning");
|
|
40
|
+
},
|
|
41
|
+
error() {
|
|
42
|
+
console.error("TypeSafe SDK error");
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return client.systemOne(request, options);
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class RequestCancelledError extends Error {
|
|
2
|
+
readonly kind: "timeout" | "aborted";
|
|
3
|
+
constructor(kind: "timeout" | "aborted");
|
|
4
|
+
}
|
|
5
|
+
export declare function withinDeadline<T>(operation: (signal: AbortSignal) => Promise<T>, options: {
|
|
6
|
+
deadlineMs: number;
|
|
7
|
+
signal?: AbortSignal | undefined;
|
|
8
|
+
}): Promise<T>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export class RequestCancelledError extends Error {
|
|
2
|
+
kind;
|
|
3
|
+
constructor(kind) {
|
|
4
|
+
super(kind);
|
|
5
|
+
this.kind = kind;
|
|
6
|
+
this.name = "RequestCancelledError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function withinDeadline(operation, options) {
|
|
10
|
+
const controller = new AbortController();
|
|
11
|
+
const cancelled = Promise.withResolvers();
|
|
12
|
+
const abort = () => {
|
|
13
|
+
controller.abort(new RequestCancelledError("aborted"));
|
|
14
|
+
};
|
|
15
|
+
const reject = () => {
|
|
16
|
+
cancelled.reject(controller.signal.reason);
|
|
17
|
+
};
|
|
18
|
+
controller.signal.addEventListener("abort", reject, { once: true });
|
|
19
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
20
|
+
const timer = setTimeout(() => controller.abort(new RequestCancelledError("timeout")), options.deadlineMs);
|
|
21
|
+
if (options.signal?.aborted)
|
|
22
|
+
abort();
|
|
23
|
+
try {
|
|
24
|
+
const request = Promise.resolve().then(() => {
|
|
25
|
+
controller.signal.throwIfAborted();
|
|
26
|
+
return operation(controller.signal);
|
|
27
|
+
});
|
|
28
|
+
return await Promise.race([request, cancelled.promise]);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
clearTimeout(timer);
|
|
32
|
+
options.signal?.removeEventListener("abort", abort);
|
|
33
|
+
controller.signal.removeEventListener("abort", reject);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ReflexStateConfig } from "../core/config.js";
|
|
2
|
+
import type { SemanticDecisions } from "../core/types.js";
|
|
3
|
+
export declare function decodeDecisions(response: unknown, ids: readonly string[], thresholds: ReflexStateConfig["thresholds"]): SemanticDecisions;
|
|
4
|
+
export declare function errorDecisions(ids: readonly string[], error: string): SemanticDecisions;
|
|
5
|
+
export declare function describeResponse(response: unknown, ids: readonly string[]): Record<string, string>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { isRecord } from "../core/serialization.js";
|
|
2
|
+
import { emptyDecisions } from "../core/updater.js";
|
|
3
|
+
import { gateChoice, gateNoul, InvalidResponseError } from "./gating.js";
|
|
4
|
+
import { categories, phases } from "./questions.js";
|
|
5
|
+
export function decodeDecisions(response, ids, thresholds) {
|
|
6
|
+
const result = record(response);
|
|
7
|
+
const answers = record(result.answers);
|
|
8
|
+
const usage = result.usage === undefined ? {} : record(result.usage);
|
|
9
|
+
return {
|
|
10
|
+
...emptyDecisions(),
|
|
11
|
+
...(ids.includes("blocker_introduced")
|
|
12
|
+
? { blockerIntroduced: gateNoul(answers.blocker_introduced, thresholds) }
|
|
13
|
+
: {}),
|
|
14
|
+
...(ids.includes("failure_category")
|
|
15
|
+
? { failureCategory: gateChoice(answers.failure_category, categories, thresholds) }
|
|
16
|
+
: {}),
|
|
17
|
+
...(ids.includes("task_complete")
|
|
18
|
+
? { taskComplete: gateNoul(answers.task_complete, thresholds) }
|
|
19
|
+
: {}),
|
|
20
|
+
...(ids.includes("phase_shadow")
|
|
21
|
+
? { phaseShadow: { ...gateChoice(answers.phase_shadow, phases, thresholds), shadow: true } }
|
|
22
|
+
: {}),
|
|
23
|
+
resolvedBlockers: ids
|
|
24
|
+
.filter((id) => id.startsWith("resolves_"))
|
|
25
|
+
.map((id) => ({
|
|
26
|
+
eventId: id.slice(9),
|
|
27
|
+
decision: gateNoul(answers[id], thresholds),
|
|
28
|
+
})),
|
|
29
|
+
relevance: ids
|
|
30
|
+
.filter((id) => id.startsWith("relevant_"))
|
|
31
|
+
.map((id) => ({
|
|
32
|
+
eventId: id.slice(9),
|
|
33
|
+
decision: gateNoul(answers[id], thresholds),
|
|
34
|
+
})),
|
|
35
|
+
telemetry: {
|
|
36
|
+
questionsAsked: ids.length,
|
|
37
|
+
questionIds: ids,
|
|
38
|
+
...(typeof result.model === "string" ? { model: result.model } : {}),
|
|
39
|
+
...(measured(usage.input_tokens) ? { inputTokens: usage.input_tokens } : {}),
|
|
40
|
+
...(measured(usage.output_tokens) ? { outputTokens: usage.output_tokens } : {}),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function record(value) {
|
|
45
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
46
|
+
throw new InvalidResponseError();
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
export function errorDecisions(ids, error) {
|
|
50
|
+
const decision = { value: null, gate: "error" };
|
|
51
|
+
return {
|
|
52
|
+
...emptyDecisions(),
|
|
53
|
+
...(ids.includes("blocker_introduced") ? { blockerIntroduced: decision } : {}),
|
|
54
|
+
...(ids.includes("failure_category") ? { failureCategory: decision } : {}),
|
|
55
|
+
...(ids.includes("task_complete") ? { taskComplete: decision } : {}),
|
|
56
|
+
...(ids.includes("phase_shadow") ? { phaseShadow: { ...decision, shadow: true } } : {}),
|
|
57
|
+
resolvedBlockers: ids
|
|
58
|
+
.filter((id) => id.startsWith("resolves_"))
|
|
59
|
+
.map((id) => ({ eventId: id.slice(9), decision })),
|
|
60
|
+
relevance: ids
|
|
61
|
+
.filter((id) => id.startsWith("relevant_"))
|
|
62
|
+
.map((id) => ({ eventId: id.slice(9), decision })),
|
|
63
|
+
telemetry: { questionsAsked: ids.length, questionIds: ids, error },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function measured(value) {
|
|
67
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
68
|
+
}
|
|
69
|
+
export function describeResponse(response, ids) {
|
|
70
|
+
const root = isRecord(response) ? response : {};
|
|
71
|
+
const answers = isRecord(root.answers) ? root.answers : {};
|
|
72
|
+
const shape = {
|
|
73
|
+
response: valueType(response),
|
|
74
|
+
answers: valueType(root.answers),
|
|
75
|
+
usage: valueType(root.usage),
|
|
76
|
+
};
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const answer = answers[id];
|
|
79
|
+
shape["answers." + id] = valueType(answer);
|
|
80
|
+
if (!isRecord(answer))
|
|
81
|
+
continue;
|
|
82
|
+
for (const field of ["noul", "choice", "confidence", "probabilities"])
|
|
83
|
+
if (Object.hasOwn(answer, field))
|
|
84
|
+
shape["answers." + id + "." + field] = valueType(answer[field]);
|
|
85
|
+
}
|
|
86
|
+
return shape;
|
|
87
|
+
}
|
|
88
|
+
function valueType(value) {
|
|
89
|
+
if (value === undefined)
|
|
90
|
+
return "missing";
|
|
91
|
+
if (value === null)
|
|
92
|
+
return "null";
|
|
93
|
+
return Array.isArray(value) ? "array" : typeof value;
|
|
94
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReflexStateConfig } from "../core/config.js";
|
|
2
|
+
import type { GatedDecision } from "../core/types.js";
|
|
3
|
+
export declare class InvalidResponseError extends Error {
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export declare function gateNoul(answer: unknown, thresholds: ReflexStateConfig["thresholds"]): GatedDecision<boolean>;
|
|
7
|
+
export declare function gateChoice<T extends string>(answer: unknown, choices: readonly T[], thresholds: ReflexStateConfig["thresholds"]): GatedDecision<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export class InvalidResponseError extends Error {
|
|
2
|
+
constructor() {
|
|
3
|
+
super("Invalid response shape from TypeSafe");
|
|
4
|
+
this.name = "InvalidResponseError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export function gateNoul(answer, thresholds) {
|
|
8
|
+
const probability = object(answer).noul;
|
|
9
|
+
if (!isProbability(probability))
|
|
10
|
+
throw new InvalidResponseError();
|
|
11
|
+
if (probability >= thresholds.noulAccept)
|
|
12
|
+
return { value: true, gate: "applied", probability };
|
|
13
|
+
if (probability <= thresholds.noulReject)
|
|
14
|
+
return { value: false, gate: "applied", probability };
|
|
15
|
+
return { value: null, gate: "uncertain", probability };
|
|
16
|
+
}
|
|
17
|
+
export function gateChoice(answer, choices, thresholds) {
|
|
18
|
+
const response = object(answer);
|
|
19
|
+
const choice = choices.find((value) => value === response.choice);
|
|
20
|
+
const confidence = response.confidence;
|
|
21
|
+
const distribution = object(response.probabilities);
|
|
22
|
+
if (!choice || !isProbability(confidence))
|
|
23
|
+
throw new InvalidResponseError();
|
|
24
|
+
const probabilities = {};
|
|
25
|
+
for (const label of choices) {
|
|
26
|
+
const probability = distribution[label];
|
|
27
|
+
if (!isProbability(probability))
|
|
28
|
+
throw new InvalidResponseError();
|
|
29
|
+
probabilities[label] = probability;
|
|
30
|
+
}
|
|
31
|
+
const margin = (probabilities[choice] ?? 0) -
|
|
32
|
+
Math.max(0, ...choices.filter((label) => label !== choice).map((label) => probabilities[label] ?? 0));
|
|
33
|
+
const applied = confidence >= thresholds.minChoiceConfidence && margin >= thresholds.minChoiceMargin;
|
|
34
|
+
return {
|
|
35
|
+
value: applied ? choice : null,
|
|
36
|
+
gate: applied ? "applied" : "uncertain",
|
|
37
|
+
confidence,
|
|
38
|
+
probabilities,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function object(value) {
|
|
42
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
43
|
+
throw new InvalidResponseError();
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
function isProbability(value) {
|
|
47
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
48
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { buildRequestPlan } from "./request_plan.js";
|
|
2
|
+
export function redact(text) {
|
|
3
|
+
return text
|
|
4
|
+
.replace(/-----BEGIN [^-]*PRIVATE KEY-----[\s\S]*?(?:-----END [^-]*PRIVATE KEY-----\s*|$)/g, "[REDACTED]")
|
|
5
|
+
.replace(/\bsk-[a-zA-Z0-9_-]{8,}/g, "[REDACTED]")
|
|
6
|
+
.replace(/\bAKIA[A-Z0-9]{16}\b/g, "[REDACTED]")
|
|
7
|
+
.replace(/\bBearer\s+[^\s"'<>]+/gi, "Bearer [REDACTED]")
|
|
8
|
+
.replace(/^(\s*(?:export\s+)?[A-Z][A-Z0-9_]*\s*=).*$/gm, "$1[REDACTED]");
|
|
9
|
+
}
|
|
10
|
+
export function buildInput(context) {
|
|
11
|
+
let excerptLimit = 1800;
|
|
12
|
+
while (true) {
|
|
13
|
+
const input = JSON.stringify(sanitize(inputObject(context, excerptLimit)));
|
|
14
|
+
if (Buffer.byteLength(input) <= 24_000)
|
|
15
|
+
return input;
|
|
16
|
+
if (excerptLimit === 0)
|
|
17
|
+
throw new Error("input_budget_exceeded");
|
|
18
|
+
excerptLimit = Math.floor(excerptLimit / 2);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function inputObject(context, excerptLimit) {
|
|
22
|
+
const { state, event, facts, evidence, config } = context;
|
|
23
|
+
const plan = buildRequestPlan(context);
|
|
24
|
+
const sources = new Map(evidence);
|
|
25
|
+
sources.set(event.id, event);
|
|
26
|
+
const goal = state.goal ? evidence.get(state.goal) : undefined;
|
|
27
|
+
const related = plan.evidenceIds.flatMap((id) => {
|
|
28
|
+
const source = sources.get(id);
|
|
29
|
+
return source && !isReadResult(source) ? [[id, inputEvent(source, excerptLimit)]] : [];
|
|
30
|
+
});
|
|
31
|
+
const call = event.type === "tool_result"
|
|
32
|
+
? [...sources.values()].find((source) => source.type === "tool_call" && source.toolCallId === event.toolCallId)
|
|
33
|
+
: undefined;
|
|
34
|
+
return {
|
|
35
|
+
schema: {
|
|
36
|
+
phase: "Current activity; done means the user's goal is complete.",
|
|
37
|
+
blocker: "An unresolved obstacle backed by an event. Verification-origin blockers are resolved by code.",
|
|
38
|
+
verification: "Observed overall command outcome; compound commands do not prove every segment ran.",
|
|
39
|
+
},
|
|
40
|
+
request_plan: {
|
|
41
|
+
questions: plan.items.map((item) => ({
|
|
42
|
+
id: item.id,
|
|
43
|
+
kind: item.kind,
|
|
44
|
+
evidence: item.requiredEvidence,
|
|
45
|
+
})),
|
|
46
|
+
skipped: plan.skipped,
|
|
47
|
+
},
|
|
48
|
+
goal: goal?.type === "user_prompt"
|
|
49
|
+
? { id: goal.id, text: redact(goal.text).slice(0, config.limits.maxPromptChars) }
|
|
50
|
+
: null,
|
|
51
|
+
current_state: {
|
|
52
|
+
phase: state.phase,
|
|
53
|
+
taskStatus: state.taskStatus,
|
|
54
|
+
verification: state.verification,
|
|
55
|
+
activeBlockers: {
|
|
56
|
+
unresolvedTotal: state.activeBlockers.length,
|
|
57
|
+
shown: state.activeBlockers.slice(-config.limits.maxProjectedBlockers),
|
|
58
|
+
omitted: Math.max(0, state.activeBlockers.length - config.limits.maxProjectedBlockers),
|
|
59
|
+
},
|
|
60
|
+
modifiedFiles: state.modifiedFiles,
|
|
61
|
+
},
|
|
62
|
+
latest_event: {
|
|
63
|
+
...inputEvent(event, excerptLimit),
|
|
64
|
+
verification: facts.verification,
|
|
65
|
+
exitCode: facts.exitCode,
|
|
66
|
+
command: call?.type === "tool_call" && typeof call.input.command === "string"
|
|
67
|
+
? redact(call.input.command).slice(0, 2000)
|
|
68
|
+
: undefined,
|
|
69
|
+
},
|
|
70
|
+
evidence: Object.fromEntries(related),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function inputEvent(event, excerptLimit) {
|
|
74
|
+
if (event.type === "tool_result")
|
|
75
|
+
return {
|
|
76
|
+
id: event.id,
|
|
77
|
+
type: event.type,
|
|
78
|
+
toolName: event.toolName,
|
|
79
|
+
isError: event.isError,
|
|
80
|
+
excerpt: ["bash", "edit", "write"].includes(event.toolName)
|
|
81
|
+
? excerptText(event.excerpt, excerptLimit)
|
|
82
|
+
: undefined,
|
|
83
|
+
};
|
|
84
|
+
if (event.type === "agent_end")
|
|
85
|
+
return {
|
|
86
|
+
id: event.id,
|
|
87
|
+
type: event.type,
|
|
88
|
+
stopReason: event.stopReason,
|
|
89
|
+
finalText: excerptText(event.finalText, excerptLimit),
|
|
90
|
+
};
|
|
91
|
+
if (event.type === "user_prompt")
|
|
92
|
+
return { id: event.id, type: event.type, text: redact(event.text).slice(0, excerptLimit) };
|
|
93
|
+
if (event.type === "file_change")
|
|
94
|
+
return { id: event.id, type: event.type, paths: event.paths };
|
|
95
|
+
if (event.type === "session_resume")
|
|
96
|
+
return { id: event.id, type: event.type, reason: event.reason };
|
|
97
|
+
return { id: event.id, type: event.type, toolName: event.toolName };
|
|
98
|
+
}
|
|
99
|
+
function excerptText(excerpt, limit) {
|
|
100
|
+
const text = redact(excerpt.head + (excerpt.tail ? "\n[excerpt gap]\n" + excerpt.tail : ""));
|
|
101
|
+
return boundedText(text, limit);
|
|
102
|
+
}
|
|
103
|
+
function isReadResult(event) {
|
|
104
|
+
return event.type === "tool_result" && ["read", "grep", "find", "ls"].includes(event.toolName);
|
|
105
|
+
}
|
|
106
|
+
function sanitize(value) {
|
|
107
|
+
if (typeof value === "string")
|
|
108
|
+
return redact(value);
|
|
109
|
+
if (Array.isArray(value))
|
|
110
|
+
return value.map(sanitize);
|
|
111
|
+
if (value && typeof value === "object")
|
|
112
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitize(item)]));
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
import { boundedText } from "../core/events.js";
|