u-foo 3.0.0 → 3.0.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/package.json +1 -1
- package/src/code/agent.js +17 -3
- package/src/code/context/executionSegment.js +5 -0
- package/src/code/context/planMode.js +8 -1
- package/src/code/index.js +2 -0
- package/src/code/nativeRunner.js +143 -207
- package/src/code/protocol/controlPlane.js +93 -0
- package/src/code/protocol/faultHarness.js +90 -0
- package/src/code/protocol/index.js +20 -0
- package/src/code/protocol/loopEvents.js +102 -0
- package/src/code/protocol/materialize.js +107 -0
- package/src/code/protocol/messageFixtures.js +116 -0
- package/src/code/protocol/ownership.js +147 -0
- package/src/code/protocol/protocolValidator.js +165 -0
- package/src/code/protocol/suspension.js +173 -0
- package/src/code/protocol/toolCallLedger.js +222 -0
- package/src/code/protocol/transitions.js +97 -0
- package/src/code/providers/anthropicMessagesTransport.js +93 -0
- package/src/code/providers/index.js +7 -0
- package/src/code/providers/openaiChatTransport.js +98 -0
- package/src/code/providers/transportContract.js +46 -0
- package/src/code/repl.js +8 -21
- package/src/code/runtime/taskLoop.js +13 -2
- package/src/code/runtime/taskRun.js +162 -1
- package/src/code/runtime/workspaceLease.js +41 -0
- package/src/code/sessionStore.js +1 -0
- package/src/code/taskRoute.js +73 -0
- package/src/ui/ink/UcodeApp.js +10 -27
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal fault-injection harness for crash/restart tests (Phase 0 skeleton).
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* armFault("after_prepare_tool_calls");
|
|
8
|
+
* await withFaultPoint("after_prepare_tool_calls", async () => { ... });
|
|
9
|
+
*
|
|
10
|
+
* Armed points throw FaultInjectedError once (or until disarmed).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const armed = new Map();
|
|
14
|
+
|
|
15
|
+
class FaultInjectedError extends Error {
|
|
16
|
+
constructor(point = "") {
|
|
17
|
+
super(`fault injected at ${point}`);
|
|
18
|
+
this.name = "FaultInjectedError";
|
|
19
|
+
this.code = "FAULT_INJECTED";
|
|
20
|
+
this.point = String(point || "");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Known hook names reserved for native loop / resume (R4 expands coverage). */
|
|
25
|
+
const FAULT_POINTS = Object.freeze([
|
|
26
|
+
"after_prepare_tool_calls",
|
|
27
|
+
"before_tool_exec",
|
|
28
|
+
"after_tool_effect",
|
|
29
|
+
"before_result_commit",
|
|
30
|
+
"after_answer_commit",
|
|
31
|
+
"before_provider_resume",
|
|
32
|
+
"after_taskrun_acquire_lease",
|
|
33
|
+
"before_parent_node_sync",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
function armFault(point = "", { times = 1, error = null } = {}) {
|
|
37
|
+
const name = String(point || "").trim();
|
|
38
|
+
if (!name) throw new Error("fault point name required");
|
|
39
|
+
armed.set(name, {
|
|
40
|
+
remaining: Math.max(1, Math.floor(Number(times) || 1)),
|
|
41
|
+
error: error || null,
|
|
42
|
+
});
|
|
43
|
+
return name;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function disarmFault(point = "") {
|
|
47
|
+
const name = String(point || "").trim();
|
|
48
|
+
if (!name) {
|
|
49
|
+
armed.clear();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
armed.delete(name);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isFaultArmed(point = "") {
|
|
56
|
+
return armed.has(String(point || "").trim());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function checkFaultPoint(point = "") {
|
|
60
|
+
const name = String(point || "").trim();
|
|
61
|
+
const entry = armed.get(name);
|
|
62
|
+
if (!entry) return;
|
|
63
|
+
entry.remaining -= 1;
|
|
64
|
+
if (entry.remaining <= 0) armed.delete(name);
|
|
65
|
+
if (entry.error) throw entry.error;
|
|
66
|
+
throw new FaultInjectedError(name);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Run fn; if point is armed, throw before invoking fn.
|
|
71
|
+
*/
|
|
72
|
+
async function withFaultPoint(point = "", fn) {
|
|
73
|
+
checkFaultPoint(point);
|
|
74
|
+
return typeof fn === "function" ? fn() : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function listArmedFaults() {
|
|
78
|
+
return Array.from(armed.keys());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
FAULT_POINTS,
|
|
83
|
+
FaultInjectedError,
|
|
84
|
+
armFault,
|
|
85
|
+
disarmFault,
|
|
86
|
+
isFaultArmed,
|
|
87
|
+
checkFaultPoint,
|
|
88
|
+
withFaultPoint,
|
|
89
|
+
listArmedFaults,
|
|
90
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Protocol layer — Tool Call Ledger, validator, ownership, transitions, fault harness.
|
|
5
|
+
*
|
|
6
|
+
* Phase 0 / R1 shadow: observe and validate; do not yet own Provider wire messages.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
...require("./toolCallLedger"),
|
|
11
|
+
...require("./protocolValidator"),
|
|
12
|
+
...require("./ownership"),
|
|
13
|
+
...require("./transitions"),
|
|
14
|
+
...require("./faultHarness"),
|
|
15
|
+
...require("./messageFixtures"),
|
|
16
|
+
...require("./materialize"),
|
|
17
|
+
...require("./suspension"),
|
|
18
|
+
...require("./controlPlane"),
|
|
19
|
+
...require("./loopEvents"),
|
|
20
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ordered Agent Loop events for UI clients (R8).
|
|
5
|
+
* Distinct from runtime/runtimeEvents.js (TaskRun wakeup mail).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const LOOP_EVENT_TYPES = Object.freeze([
|
|
9
|
+
"thinking_delta",
|
|
10
|
+
"assistant_delta",
|
|
11
|
+
"tool_start",
|
|
12
|
+
"tool_result",
|
|
13
|
+
"artifact_persisted",
|
|
14
|
+
"interaction_requested",
|
|
15
|
+
"interaction_rejected",
|
|
16
|
+
"interaction_resuming",
|
|
17
|
+
"interaction_resolved",
|
|
18
|
+
"interaction_failed",
|
|
19
|
+
"plan_transition",
|
|
20
|
+
"task_run_transition",
|
|
21
|
+
"final_assistant_message",
|
|
22
|
+
"final_summary",
|
|
23
|
+
"error",
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
function createLoopEvent(type = "", payload = {}, {
|
|
27
|
+
sessionId = "",
|
|
28
|
+
runId = "",
|
|
29
|
+
sequence = 0,
|
|
30
|
+
eventId = "",
|
|
31
|
+
} = {}) {
|
|
32
|
+
const eventType = String(type || "").trim();
|
|
33
|
+
if (!LOOP_EVENT_TYPES.includes(eventType)) {
|
|
34
|
+
throw new Error(`unknown loop event type: ${type}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
eventId: String(eventId || `le_${Date.now().toString(36)}_${sequence}`),
|
|
38
|
+
sequence: Number(sequence) || 0,
|
|
39
|
+
sessionId: String(sessionId || ""),
|
|
40
|
+
runId: String(runId || ""),
|
|
41
|
+
type: eventType,
|
|
42
|
+
payload: payload && typeof payload === "object" ? payload : {},
|
|
43
|
+
timestamp: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* UI display policy: whether to echo a final summary after streaming.
|
|
49
|
+
* Prefer final_summary / final_assistant_message events when present.
|
|
50
|
+
*/
|
|
51
|
+
function resolveSummaryDisplayPolicy({
|
|
52
|
+
streamed = false,
|
|
53
|
+
sawVisibleText = false,
|
|
54
|
+
finalEventType = "",
|
|
55
|
+
} = {}) {
|
|
56
|
+
const finalType = String(finalEventType || "").trim();
|
|
57
|
+
if (finalType === "final_assistant_message" || finalType === "final_summary") {
|
|
58
|
+
return { echoSummary: true, reason: "explicit_final_event" };
|
|
59
|
+
}
|
|
60
|
+
if (streamed && sawVisibleText) {
|
|
61
|
+
return { echoSummary: false, reason: "already_streamed" };
|
|
62
|
+
}
|
|
63
|
+
return { echoSummary: true, reason: "fallback_summary" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createLoopEventLog({ sessionId = "", runId = "" } = {}) {
|
|
67
|
+
const events = [];
|
|
68
|
+
let sequence = 0;
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
|
|
71
|
+
function push(type, payload = {}) {
|
|
72
|
+
sequence += 1;
|
|
73
|
+
const event = createLoopEvent(type, payload, {
|
|
74
|
+
sessionId,
|
|
75
|
+
runId,
|
|
76
|
+
sequence,
|
|
77
|
+
});
|
|
78
|
+
if (seen.has(event.eventId)) return null;
|
|
79
|
+
seen.add(event.eventId);
|
|
80
|
+
events.push(event);
|
|
81
|
+
return event;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function replayFrom(sequenceCheckpoint = 0) {
|
|
85
|
+
const min = Number(sequenceCheckpoint) || 0;
|
|
86
|
+
return events.filter((e) => e.sequence > min);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
push,
|
|
91
|
+
replayFrom,
|
|
92
|
+
list: () => events.slice(),
|
|
93
|
+
get sequence() { return sequence; },
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
LOOP_EVENT_TYPES,
|
|
99
|
+
createLoopEvent,
|
|
100
|
+
createLoopEventLog,
|
|
101
|
+
resolveSummaryDisplayPolicy,
|
|
102
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Materialize Provider tool-result messages from a resolved ledger turn.
|
|
5
|
+
* Business branches must resolve on the ledger; they must not hand-write
|
|
6
|
+
* unpaired tool results.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { listCalls } = require("./toolCallLedger");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Clip helper kept local so protocol does not depend on nativeRunner.
|
|
13
|
+
*/
|
|
14
|
+
function clipText(value = "", maxChars = 12000) {
|
|
15
|
+
const text = String(value == null ? "" : value);
|
|
16
|
+
if (text.length <= maxChars) return text;
|
|
17
|
+
return `${text.slice(0, maxChars)}\n...[truncated ${text.length - maxChars} chars]`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function toJsonString(value) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.stringify(value);
|
|
23
|
+
} catch {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} ledger
|
|
30
|
+
* @param {{
|
|
31
|
+
* transport: object,
|
|
32
|
+
* messages: object[],
|
|
33
|
+
* pendingById: Record<string, object>,
|
|
34
|
+
* }} opts
|
|
35
|
+
* @returns {{ appended: number, flushed: boolean }}
|
|
36
|
+
*/
|
|
37
|
+
function materializeResolvedToolResults(ledger, {
|
|
38
|
+
transport = null,
|
|
39
|
+
messages = [],
|
|
40
|
+
pendingById = {},
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (!ledger || !transport || typeof transport.appendToolResult !== "function") {
|
|
43
|
+
return { appended: 0, flushed: false };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const collected = [];
|
|
47
|
+
let appended = 0;
|
|
48
|
+
for (const call of listCalls(ledger)) {
|
|
49
|
+
if (call.state !== "resolved") continue;
|
|
50
|
+
const pending = pendingById[call.callId];
|
|
51
|
+
if (!pending) continue;
|
|
52
|
+
transport.appendToolResult({
|
|
53
|
+
messages,
|
|
54
|
+
collected,
|
|
55
|
+
call: pending,
|
|
56
|
+
toolResult: call.resultPayload,
|
|
57
|
+
});
|
|
58
|
+
appended += 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let flushed = false;
|
|
62
|
+
if (
|
|
63
|
+
collected.length > 0
|
|
64
|
+
&& typeof transport.flushToolResults === "function"
|
|
65
|
+
) {
|
|
66
|
+
transport.flushToolResults({ messages, collected });
|
|
67
|
+
flushed = true;
|
|
68
|
+
}
|
|
69
|
+
return { appended, flushed };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Append a single resume answer as a tool result (idempotent via ledger resolve).
|
|
74
|
+
*/
|
|
75
|
+
function materializeAnswerToolResult(messages = [], resume = null, answer = {}, {
|
|
76
|
+
clip = clipText,
|
|
77
|
+
} = {}) {
|
|
78
|
+
const call = resume && resume.call ? resume.call : null;
|
|
79
|
+
if (!call || !call.source) return { ok: false, error: "missing deferred tool call" };
|
|
80
|
+
const transportName = String(resume.transport || "openai-chat");
|
|
81
|
+
const content = clip(toJsonString(answer), 12000);
|
|
82
|
+
if (transportName === "anthropic-messages") {
|
|
83
|
+
messages.push({
|
|
84
|
+
role: "user",
|
|
85
|
+
content: [{
|
|
86
|
+
type: "tool_result",
|
|
87
|
+
tool_use_id: String(call.source.id || resume.toolCallId || ""),
|
|
88
|
+
content,
|
|
89
|
+
is_error: false,
|
|
90
|
+
}],
|
|
91
|
+
});
|
|
92
|
+
} else {
|
|
93
|
+
messages.push({
|
|
94
|
+
role: "tool",
|
|
95
|
+
tool_call_id: String(call.source.id || resume.toolCallId || ""),
|
|
96
|
+
content,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
materializeResolvedToolResults,
|
|
104
|
+
materializeAnswerToolResult,
|
|
105
|
+
clipText,
|
|
106
|
+
toJsonString,
|
|
107
|
+
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Golden / expected Provider message sequences derived from ledger call records.
|
|
5
|
+
* Used for fixture tests only — does not replace TRANSPORTS materialization (yet).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function materializeOpenAiMessages({
|
|
9
|
+
assistantText = null,
|
|
10
|
+
calls = [],
|
|
11
|
+
results = [],
|
|
12
|
+
} = {}) {
|
|
13
|
+
const messages = [];
|
|
14
|
+
const toolCalls = calls.map((call) => ({
|
|
15
|
+
id: call.callId,
|
|
16
|
+
type: "function",
|
|
17
|
+
function: {
|
|
18
|
+
name: call.name,
|
|
19
|
+
arguments: typeof call.argsJson === "string"
|
|
20
|
+
? call.argsJson
|
|
21
|
+
: JSON.stringify(call.args == null ? {} : call.args),
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
if (toolCalls.length > 0) {
|
|
26
|
+
messages.push({
|
|
27
|
+
role: "assistant",
|
|
28
|
+
content: assistantText,
|
|
29
|
+
tool_calls: toolCalls,
|
|
30
|
+
});
|
|
31
|
+
} else if (assistantText != null) {
|
|
32
|
+
messages.push({ role: "assistant", content: assistantText });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
for (const result of results) {
|
|
36
|
+
messages.push({
|
|
37
|
+
role: "tool",
|
|
38
|
+
tool_call_id: result.callId,
|
|
39
|
+
content: typeof result.content === "string"
|
|
40
|
+
? result.content
|
|
41
|
+
: JSON.stringify(result.content == null ? {} : result.content),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return messages;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function materializeAnthropicMessages({
|
|
48
|
+
assistantBlocks = null,
|
|
49
|
+
calls = [],
|
|
50
|
+
results = [],
|
|
51
|
+
assistantText = "",
|
|
52
|
+
} = {}) {
|
|
53
|
+
const messages = [];
|
|
54
|
+
let content = Array.isArray(assistantBlocks) ? assistantBlocks.slice() : null;
|
|
55
|
+
if (!content) {
|
|
56
|
+
content = [];
|
|
57
|
+
if (assistantText) {
|
|
58
|
+
content.push({ type: "text", text: String(assistantText) });
|
|
59
|
+
}
|
|
60
|
+
for (const call of calls) {
|
|
61
|
+
content.push({
|
|
62
|
+
type: "tool_use",
|
|
63
|
+
id: call.callId,
|
|
64
|
+
name: call.name,
|
|
65
|
+
input: call.args == null ? {} : call.args,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (content.length > 0) {
|
|
70
|
+
messages.push({ role: "assistant", content });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (results.length > 0) {
|
|
74
|
+
messages.push({
|
|
75
|
+
role: "user",
|
|
76
|
+
content: results.map((result) => ({
|
|
77
|
+
type: "tool_result",
|
|
78
|
+
tool_use_id: result.callId,
|
|
79
|
+
content: typeof result.content === "string"
|
|
80
|
+
? result.content
|
|
81
|
+
: JSON.stringify(result.content == null ? {} : result.content),
|
|
82
|
+
is_error: Boolean(result.isError),
|
|
83
|
+
})),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return messages;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Build expected messages from a simplified fixture definition.
|
|
91
|
+
* @param {{ provider: string, calls: object[], results?: object[], assistantText?: string }} def
|
|
92
|
+
*/
|
|
93
|
+
function materializeFromFixtureDef(def = {}) {
|
|
94
|
+
const provider = String(def.provider || "openai").toLowerCase();
|
|
95
|
+
const calls = Array.isArray(def.calls) ? def.calls : [];
|
|
96
|
+
const results = Array.isArray(def.results) ? def.results : [];
|
|
97
|
+
if (provider === "anthropic" || provider === "anthropic-messages") {
|
|
98
|
+
return materializeAnthropicMessages({
|
|
99
|
+
calls,
|
|
100
|
+
results,
|
|
101
|
+
assistantText: def.assistantText || "",
|
|
102
|
+
assistantBlocks: def.assistantBlocks || null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
return materializeOpenAiMessages({
|
|
106
|
+
calls,
|
|
107
|
+
results,
|
|
108
|
+
assistantText: def.assistantText != null ? def.assistantText : null,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = {
|
|
113
|
+
materializeOpenAiMessages,
|
|
114
|
+
materializeAnthropicMessages,
|
|
115
|
+
materializeFromFixtureDef,
|
|
116
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Session state ownership table (Phase 0).
|
|
5
|
+
*
|
|
6
|
+
* Durable fields are recovery sources. Projections may be rebuilt from durable
|
|
7
|
+
* state (or from protocolLedger + provider message checkpoint until full
|
|
8
|
+
* event-sourced rebuild exists).
|
|
9
|
+
*
|
|
10
|
+
* See docs/ucode-agent-runtime-remediation-plan.md §4.2 / R4.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** @typedef {"session"|"session/artifacts"|"checkpoint"|"runtime_events"|"execution/session"} Authority */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @type {ReadonlyArray<{
|
|
17
|
+
* key: string,
|
|
18
|
+
* authority: Authority,
|
|
19
|
+
* durable: boolean,
|
|
20
|
+
* rebuildable: boolean,
|
|
21
|
+
* notes?: string
|
|
22
|
+
* }>}
|
|
23
|
+
*/
|
|
24
|
+
const STATE_OWNERSHIP = Object.freeze([
|
|
25
|
+
{
|
|
26
|
+
key: "protocolLedger",
|
|
27
|
+
authority: "session",
|
|
28
|
+
durable: true,
|
|
29
|
+
rebuildable: false,
|
|
30
|
+
notes: "Not yet persisted; planned under DurableSessionState (R1/R4)",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
key: "executionState",
|
|
34
|
+
authority: "session",
|
|
35
|
+
durable: true,
|
|
36
|
+
rebuildable: false,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: "transcript.events",
|
|
40
|
+
authority: "session/artifacts",
|
|
41
|
+
durable: true,
|
|
42
|
+
rebuildable: false,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "providerMessages",
|
|
46
|
+
authority: "checkpoint",
|
|
47
|
+
durable: true,
|
|
48
|
+
rebuildable: true,
|
|
49
|
+
notes: "Today: nlMessages stripped on save; rebuilt from transcript when possible",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
key: "workingSet",
|
|
53
|
+
authority: "execution/session",
|
|
54
|
+
durable: true,
|
|
55
|
+
rebuildable: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
key: "artifacts",
|
|
59
|
+
authority: "session/artifacts",
|
|
60
|
+
durable: true,
|
|
61
|
+
rebuildable: false,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
key: "rollingSummary",
|
|
65
|
+
authority: "session",
|
|
66
|
+
durable: true,
|
|
67
|
+
rebuildable: true,
|
|
68
|
+
notes: "Optional durable projection (summary field)",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "uiLogs",
|
|
72
|
+
authority: "runtime_events",
|
|
73
|
+
durable: false,
|
|
74
|
+
rebuildable: true,
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
key: "planUi",
|
|
78
|
+
authority: "execution/session",
|
|
79
|
+
durable: true,
|
|
80
|
+
rebuildable: true,
|
|
81
|
+
notes: "Band mode preferences; graph view is projection of planGraph",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "sessionStatus",
|
|
85
|
+
authority: "runtime_events",
|
|
86
|
+
durable: false,
|
|
87
|
+
rebuildable: true,
|
|
88
|
+
},
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
const DURABLE_FIELDS = Object.freeze(
|
|
92
|
+
STATE_OWNERSHIP.filter((row) => row.durable).map((row) => row.key)
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const PROJECTION_FIELDS = Object.freeze(
|
|
96
|
+
STATE_OWNERSHIP.filter((row) => row.rebuildable).map((row) => row.key)
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
function getOwnershipRow(key = "") {
|
|
100
|
+
const id = String(key || "").trim();
|
|
101
|
+
return STATE_OWNERSHIP.find((row) => row.key === id) || null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertOwnershipTableInvariants() {
|
|
105
|
+
const keys = new Set();
|
|
106
|
+
for (const row of STATE_OWNERSHIP) {
|
|
107
|
+
if (!row.key) throw new Error("ownership row missing key");
|
|
108
|
+
if (keys.has(row.key)) throw new Error(`duplicate ownership key: ${row.key}`);
|
|
109
|
+
keys.add(row.key);
|
|
110
|
+
if (typeof row.durable !== "boolean" || typeof row.rebuildable !== "boolean") {
|
|
111
|
+
throw new Error(`ownership row ${row.key} has invalid flags`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Atomic commit boundaries (R4). Crash between these stages must have a
|
|
119
|
+
* defined recovery; tools that are not naturally idempotent must not be
|
|
120
|
+
* blindly re-executed when stopped at `started`.
|
|
121
|
+
*/
|
|
122
|
+
const ATOMIC_COMMIT_BOUNDARIES = Object.freeze([
|
|
123
|
+
"assistant_tool_calls_accepted",
|
|
124
|
+
"before_side_effect_tool",
|
|
125
|
+
"after_side_effect_persisted",
|
|
126
|
+
"tool_result_committed_to_ledger",
|
|
127
|
+
"before_agent_loop_suspension",
|
|
128
|
+
"after_resume_answer_committed",
|
|
129
|
+
"after_taskrun_terminal_cas",
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
const SIDE_EFFECT_INVOCATION_PHASES = Object.freeze([
|
|
133
|
+
"prepared",
|
|
134
|
+
"started",
|
|
135
|
+
"effect_observed",
|
|
136
|
+
"result_committed",
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
STATE_OWNERSHIP,
|
|
141
|
+
DURABLE_FIELDS,
|
|
142
|
+
PROJECTION_FIELDS,
|
|
143
|
+
getOwnershipRow,
|
|
144
|
+
assertOwnershipTableInvariants,
|
|
145
|
+
ATOMIC_COMMIT_BOUNDARIES,
|
|
146
|
+
SIDE_EFFECT_INVOCATION_PHASES,
|
|
147
|
+
};
|