wayang-ai 0.5.0 → 0.5.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/dist/cli.js +1082 -228
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -156,7 +156,11 @@ var METHOD = {
|
|
|
156
156
|
GET_SNAPSHOT: "get_snapshot",
|
|
157
157
|
SUBSCRIBE: "subscribe",
|
|
158
158
|
UNSUBSCRIBE: "unsubscribe",
|
|
159
|
-
SHUTDOWN: "shutdown"
|
|
159
|
+
SHUTDOWN: "shutdown",
|
|
160
|
+
/** Read-only: list turns of the controller conversation (limit/offset). */
|
|
161
|
+
CONVERSATION_LIST_TURNS: "conversation_list_turns",
|
|
162
|
+
/** Read-only: one turn of the controller conversation by turnId. */
|
|
163
|
+
CONVERSATION_GET_TURN: "conversation_get_turn"
|
|
160
164
|
};
|
|
161
165
|
var NOTIFY = {
|
|
162
166
|
READY: "ready",
|
|
@@ -819,14 +823,8 @@ function ToolResultRow({ section: section2 }) {
|
|
|
819
823
|
import { Box as Box3, Text as Text3 } from "ink";
|
|
820
824
|
|
|
821
825
|
// src/types/conversation.ts
|
|
822
|
-
function
|
|
823
|
-
return entry.type === "system" /* System
|
|
824
|
-
}
|
|
825
|
-
function getEntryContent(entry) {
|
|
826
|
-
if (entry.type === "signal" /* Signal */ || entry.type === "system" /* System */) {
|
|
827
|
-
return entry.content;
|
|
828
|
-
}
|
|
829
|
-
return entry.message.content;
|
|
826
|
+
function isCompactMarker(entry) {
|
|
827
|
+
return entry.type === "system" /* System */ && entry.subtype === "compact" /* Compact */ && entry.compactMeta !== void 0;
|
|
830
828
|
}
|
|
831
829
|
|
|
832
830
|
// src/tui/components/chat-area/signal-row.tsx
|
|
@@ -868,12 +866,19 @@ function SignalRow({ item }) {
|
|
|
868
866
|
// src/tui/components/chat-area/system-row.tsx
|
|
869
867
|
import { Box as Box4, Text as Text4 } from "ink";
|
|
870
868
|
import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
869
|
+
function isCompactSystemItem(item) {
|
|
870
|
+
return item.subtype === "compact" /* Compact */ && item.compactMeta !== void 0;
|
|
871
|
+
}
|
|
871
872
|
function SystemRow({ item }) {
|
|
872
|
-
const isError = item.subtype === "error"
|
|
873
|
-
const isCompact = item
|
|
873
|
+
const isError = item.subtype === "error" /* Error */;
|
|
874
|
+
const isCompact = isCompactSystemItem(item);
|
|
875
|
+
const isTaskResult = item.subtype === "task_result" /* TaskResult */;
|
|
874
876
|
if (isCompact) {
|
|
875
877
|
return /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "\u224B Conversation has been compacted" }) });
|
|
876
878
|
}
|
|
879
|
+
if (isTaskResult) {
|
|
880
|
+
return /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: item.content }) });
|
|
881
|
+
}
|
|
877
882
|
return /* @__PURE__ */ jsxs4(Box4, { children: [
|
|
878
883
|
/* @__PURE__ */ jsxs4(Text4, { color: isError ? theme.baseToken.color.error : theme.meta.color, children: [
|
|
879
884
|
isError ? "\u2718" : theme.meta.prefix,
|
|
@@ -1396,7 +1401,7 @@ function entryToDisplayItem(entry) {
|
|
|
1396
1401
|
case "signal" /* Signal */:
|
|
1397
1402
|
return { ...base, role: "signal", subtype: entry.subtype, content: entry.content, taskId: entry.taskId, taskTitle: entry.taskTitle, workerId: entry.workerId, workerType: entry.workerType, emoji: entry.emoji };
|
|
1398
1403
|
case "system" /* System */:
|
|
1399
|
-
return { ...base, role: "system", subtype: entry.subtype, content: entry.content };
|
|
1404
|
+
return { ...base, role: "system", subtype: entry.subtype, content: entry.content, compactMeta: entry.compactMeta };
|
|
1400
1405
|
}
|
|
1401
1406
|
}
|
|
1402
1407
|
var signalSubtypeMap = {
|
|
@@ -1404,7 +1409,8 @@ var signalSubtypeMap = {
|
|
|
1404
1409
|
failed: "worker_failed" /* WorkerFailed */,
|
|
1405
1410
|
progress: "worker_progress" /* WorkerProgress */,
|
|
1406
1411
|
heartbeat: "heartbeat" /* Heartbeat */,
|
|
1407
|
-
cancelled: "worker_failed" /* WorkerFailed
|
|
1412
|
+
cancelled: "worker_failed" /* WorkerFailed */,
|
|
1413
|
+
task_cancelled: "task_cancelled" /* TaskCancelled */
|
|
1408
1414
|
};
|
|
1409
1415
|
function signalToDisplayItem(signal) {
|
|
1410
1416
|
const base = {
|
|
@@ -1570,7 +1576,7 @@ function AppContent() {
|
|
|
1570
1576
|
// package.json
|
|
1571
1577
|
var package_default = {
|
|
1572
1578
|
name: "wayang-ai",
|
|
1573
|
-
version: "0.5.
|
|
1579
|
+
version: "0.5.1",
|
|
1574
1580
|
description: "Multi-agent orchestration platform",
|
|
1575
1581
|
type: "module",
|
|
1576
1582
|
license: "MIT",
|
|
@@ -2034,6 +2040,21 @@ import { join as join5 } from "path";
|
|
|
2034
2040
|
|
|
2035
2041
|
// src/infra/state/base-state.ts
|
|
2036
2042
|
import { get, set, cloneDeep } from "lodash-es";
|
|
2043
|
+
|
|
2044
|
+
// src/infra/state/conversation-envelope.ts
|
|
2045
|
+
var ENVELOPE_VERSION = 1;
|
|
2046
|
+
function applyAppendEnvelope(entry, conversation) {
|
|
2047
|
+
if (entry === null || typeof entry !== "object") return entry;
|
|
2048
|
+
if (entry.v === void 0) entry.v = ENVELOPE_VERSION;
|
|
2049
|
+
if (entry.parentId === void 0) {
|
|
2050
|
+
const last = conversation.length > 0 ? conversation[conversation.length - 1] : void 0;
|
|
2051
|
+
const lastIsMarker = last !== null && typeof last === "object" && isCompactMarker(last);
|
|
2052
|
+
entry.parentId = !last || lastIsMarker || isCompactMarker(entry) || typeof last.uuid !== "string" ? null : last.uuid;
|
|
2053
|
+
}
|
|
2054
|
+
return entry;
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// src/infra/state/base-state.ts
|
|
2037
2058
|
var BaseWayangState = class {
|
|
2038
2059
|
data;
|
|
2039
2060
|
persistMappings;
|
|
@@ -2069,9 +2090,10 @@ var BaseWayangState = class {
|
|
|
2069
2090
|
if (!Array.isArray(arr)) {
|
|
2070
2091
|
throw new Error(`State.append: path "${path7}" is not an array`);
|
|
2071
2092
|
}
|
|
2072
|
-
|
|
2093
|
+
const stamped = path7 === "conversation" ? applyAppendEnvelope(entry, arr) : entry;
|
|
2094
|
+
arr.push(stamped);
|
|
2073
2095
|
set(this.data, path7, [...arr]);
|
|
2074
|
-
this.persist(path7, "append",
|
|
2096
|
+
this.persist(path7, "append", stamped);
|
|
2075
2097
|
this.notify(path7, "append", void 0);
|
|
2076
2098
|
}
|
|
2077
2099
|
remove(path7, idx) {
|
|
@@ -2714,6 +2736,18 @@ function formatTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
|
2714
2736
|
return `${Y}${M}${D}-${h}${m}${s}`;
|
|
2715
2737
|
}
|
|
2716
2738
|
|
|
2739
|
+
// src/utils/time.ts
|
|
2740
|
+
function nowISO() {
|
|
2741
|
+
const now = /* @__PURE__ */ new Date();
|
|
2742
|
+
const off = -now.getTimezoneOffset();
|
|
2743
|
+
const sign = off >= 0 ? "+" : "-";
|
|
2744
|
+
const absOff = Math.abs(off);
|
|
2745
|
+
const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
|
|
2746
|
+
const mm = String(absOff % 60).padStart(2, "0");
|
|
2747
|
+
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
2748
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}${sign}${hh}:${mm}`;
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2717
2751
|
// src/services/agents/base-agent.ts
|
|
2718
2752
|
var BaseAgent = class {
|
|
2719
2753
|
id;
|
|
@@ -2742,9 +2776,18 @@ var BaseAgent = class {
|
|
|
2742
2776
|
}
|
|
2743
2777
|
}
|
|
2744
2778
|
let startTime;
|
|
2779
|
+
let firstChunkAt;
|
|
2780
|
+
let stepStartedAt;
|
|
2781
|
+
const onStep = opts.onStep ? (event) => {
|
|
2782
|
+
const meta = { startedAt: stepStartedAt, firstTokenAt: firstChunkAt };
|
|
2783
|
+
firstChunkAt = void 0;
|
|
2784
|
+
stepStartedAt = nowISO();
|
|
2785
|
+
opts.onStep(event, meta);
|
|
2786
|
+
} : void 0;
|
|
2745
2787
|
try {
|
|
2746
2788
|
this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
|
|
2747
2789
|
startTime = Date.now();
|
|
2790
|
+
stepStartedAt = nowISO();
|
|
2748
2791
|
stream = streamText({
|
|
2749
2792
|
model: this.model,
|
|
2750
2793
|
system: opts.system,
|
|
@@ -2753,7 +2796,7 @@ var BaseAgent = class {
|
|
|
2753
2796
|
toolChoice: opts.toolChoice,
|
|
2754
2797
|
stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
|
|
2755
2798
|
abortSignal: this.abortController.signal,
|
|
2756
|
-
onStepFinish:
|
|
2799
|
+
onStepFinish: onStep,
|
|
2757
2800
|
onError: ({ error }) => {
|
|
2758
2801
|
opts.onStreamError?.(error);
|
|
2759
2802
|
},
|
|
@@ -2770,6 +2813,7 @@ var BaseAgent = class {
|
|
|
2770
2813
|
}
|
|
2771
2814
|
try {
|
|
2772
2815
|
for await (const chunk of stream.textStream) {
|
|
2816
|
+
if (chunk && firstChunkAt === void 0) firstChunkAt = nowISO();
|
|
2773
2817
|
yield chunk;
|
|
2774
2818
|
}
|
|
2775
2819
|
} catch (err) {
|
|
@@ -2913,37 +2957,30 @@ function estimateTokens(text) {
|
|
|
2913
2957
|
if (!text) return 0;
|
|
2914
2958
|
return Math.ceil(text.length / 4);
|
|
2915
2959
|
}
|
|
2960
|
+
function estimateEntryTokens(entry) {
|
|
2961
|
+
if (!entry) return 0;
|
|
2962
|
+
const content = entry.message?.content ?? entry.content ?? "";
|
|
2963
|
+
let tokens = estimateTokens(String(content ?? ""));
|
|
2964
|
+
if (entry.toolCalls?.length) {
|
|
2965
|
+
for (const tc of entry.toolCalls) {
|
|
2966
|
+
tokens += estimateTokens(tc.arguments ?? "");
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
if (entry.toolResults?.length) {
|
|
2970
|
+
for (const tr of entry.toolResults) {
|
|
2971
|
+
tokens += estimateTokens(tr.output?.value ?? "");
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
return tokens;
|
|
2975
|
+
}
|
|
2916
2976
|
function estimateConversationTokens(entries) {
|
|
2917
2977
|
let total = 0;
|
|
2918
2978
|
for (const entry of entries) {
|
|
2919
|
-
|
|
2920
|
-
total += estimateTokens(entry.message.content);
|
|
2921
|
-
}
|
|
2922
|
-
if (entry.arguments) {
|
|
2923
|
-
total += estimateTokens(typeof entry.arguments === "string" ? entry.arguments : JSON.stringify(entry.arguments));
|
|
2924
|
-
}
|
|
2925
|
-
if (entry.result) {
|
|
2926
|
-
total += estimateTokens(entry.result);
|
|
2927
|
-
}
|
|
2928
|
-
if (entry.content) {
|
|
2929
|
-
total += estimateTokens(entry.content);
|
|
2930
|
-
}
|
|
2979
|
+
total += estimateEntryTokens(entry);
|
|
2931
2980
|
}
|
|
2932
2981
|
return total;
|
|
2933
2982
|
}
|
|
2934
2983
|
|
|
2935
|
-
// src/utils/time.ts
|
|
2936
|
-
function nowISO() {
|
|
2937
|
-
const now = /* @__PURE__ */ new Date();
|
|
2938
|
-
const off = -now.getTimezoneOffset();
|
|
2939
|
-
const sign = off >= 0 ? "+" : "-";
|
|
2940
|
-
const absOff = Math.abs(off);
|
|
2941
|
-
const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
|
|
2942
|
-
const mm = String(absOff % 60).padStart(2, "0");
|
|
2943
|
-
const pad = (n, len = 2) => String(n).padStart(len, "0");
|
|
2944
|
-
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}${sign}${hh}:${mm}`;
|
|
2945
|
-
}
|
|
2946
|
-
|
|
2947
2984
|
// src/services/agents/context-manager.ts
|
|
2948
2985
|
var DEFAULT_MAX_TOKENS = 1e5;
|
|
2949
2986
|
var COMPACT_THRESHOLD_RATIO = 0.9;
|
|
@@ -2959,13 +2996,22 @@ var ContextManager = class {
|
|
|
2959
2996
|
staticPrompt;
|
|
2960
2997
|
maxTokens;
|
|
2961
2998
|
/**
|
|
2962
|
-
* Cached compact state — updated by compact() and
|
|
2963
|
-
* Null means no compaction
|
|
2999
|
+
* Cached compact state — updated by compact() and restoreCompact(), read by
|
|
3000
|
+
* getMessages()/getSystemPrompt(). Null means no compaction is active.
|
|
2964
3001
|
*/
|
|
2965
3002
|
compactSummary = null;
|
|
2966
3003
|
/**
|
|
2967
|
-
* Index into the conversation array: entries from this index onward
|
|
2968
|
-
* "
|
|
3004
|
+
* Index into the conversation array: entries from this index onward form the
|
|
3005
|
+
* "verbatim zone" — entries sent to the LLM as-is alongside the summary.
|
|
3006
|
+
*
|
|
3007
|
+
* Semantics (unified):
|
|
3008
|
+
* - After compact(): conversation.length. Only entries appended AFTER the
|
|
3009
|
+
* compaction belong to the verbatim zone; pre-compaction entries live on
|
|
3010
|
+
* in the summary (and in the append-only JSONL history).
|
|
3011
|
+
* - After restoreCompact(): 0. The state layer has already sliced the
|
|
3012
|
+
* in-memory conversation to the restore window, so the whole array IS
|
|
3013
|
+
* the verbatim zone.
|
|
3014
|
+
*
|
|
2969
3015
|
* Updated together with compactSummary.
|
|
2970
3016
|
*/
|
|
2971
3017
|
compactStartIdx = 0;
|
|
@@ -3004,6 +3050,22 @@ ${this.compactSummary}` },
|
|
|
3004
3050
|
const convTokens = estimateConversationTokens(this.getMessages());
|
|
3005
3051
|
return promptTokens + convTokens > this.maxTokens * COMPACT_THRESHOLD_RATIO;
|
|
3006
3052
|
}
|
|
3053
|
+
/**
|
|
3054
|
+
* Restore compact state from a summary computed by the state layer.
|
|
3055
|
+
*
|
|
3056
|
+
* Call this after the state layer's restore() has already sliced the
|
|
3057
|
+
* in-memory conversation to the restore window (entries before the cut are
|
|
3058
|
+
* gone from memory). The window itself is the verbatim zone, so
|
|
3059
|
+
* compactStartIdx resets to 0 — see the field's semantics note.
|
|
3060
|
+
*/
|
|
3061
|
+
restoreCompact(summary) {
|
|
3062
|
+
this.compactSummary = summary;
|
|
3063
|
+
this.compactStartIdx = 0;
|
|
3064
|
+
}
|
|
3065
|
+
/** Get the cached compaction summary, or null if no compaction is active. */
|
|
3066
|
+
getCompactSummary() {
|
|
3067
|
+
return this.compactSummary;
|
|
3068
|
+
}
|
|
3007
3069
|
/**
|
|
3008
3070
|
* Compact the conversation by summarizing all current entries.
|
|
3009
3071
|
*
|
|
@@ -3012,72 +3074,40 @@ ${this.compactSummary}` },
|
|
|
3012
3074
|
*
|
|
3013
3075
|
* The summarizer receives ALL entries and returns a summary.
|
|
3014
3076
|
* After compaction, getMessages() returns [summary] + entries appended after the marker.
|
|
3077
|
+
*
|
|
3078
|
+
* @param meta Optional provenance recorded on the marker (model name).
|
|
3015
3079
|
*/
|
|
3016
|
-
async compact(summarizer) {
|
|
3080
|
+
async compact(summarizer, meta) {
|
|
3017
3081
|
const conversation = this.state.get("conversation");
|
|
3018
3082
|
if (conversation.length < 4) return;
|
|
3083
|
+
const fromEntryId = conversation[0]?.uuid;
|
|
3084
|
+
const toEntryId = conversation[conversation.length - 1]?.uuid;
|
|
3019
3085
|
const summary = await summarizer(conversation);
|
|
3020
3086
|
this.compactSummary = summary;
|
|
3021
3087
|
this.compactStartIdx = conversation.length;
|
|
3088
|
+
const markerTs = nowISO();
|
|
3022
3089
|
this.state.append("conversation", {
|
|
3023
3090
|
type: "system" /* System */,
|
|
3024
3091
|
uuid: generateId("compact"),
|
|
3025
3092
|
parentUuid: null,
|
|
3026
3093
|
sessionId: this.state.get("runtimeState.session.id") ?? "",
|
|
3027
|
-
timestamp:
|
|
3094
|
+
timestamp: markerTs,
|
|
3028
3095
|
subtype: "compact" /* Compact */,
|
|
3029
3096
|
content: summary,
|
|
3097
|
+
// System entries are point-in-time events: startedAt/endedAt share the
|
|
3098
|
+
// marker's creation moment.
|
|
3099
|
+
startedAt: markerTs,
|
|
3100
|
+
endedAt: markerTs,
|
|
3030
3101
|
compactMeta: {
|
|
3031
|
-
summarizedTokenCount: estimateConversationTokens(conversation)
|
|
3102
|
+
summarizedTokenCount: estimateConversationTokens(conversation),
|
|
3103
|
+
...fromEntryId !== void 0 && { fromEntryId },
|
|
3104
|
+
...toEntryId !== void 0 && { toEntryId },
|
|
3105
|
+
...meta?.model !== void 0 && { model: meta.model },
|
|
3106
|
+
createdAt: markerTs
|
|
3032
3107
|
}
|
|
3033
3108
|
});
|
|
3034
3109
|
}
|
|
3035
|
-
/**
|
|
3036
|
-
* Restore compact state from persisted conversation.
|
|
3037
|
-
*
|
|
3038
|
-
* Scans for the latest compact marker, then loads recent entries within
|
|
3039
|
-
* the token budget. If loaded entries are fewer than RECENT_TOKEN_BUDGET,
|
|
3040
|
-
* continues loading past the marker (overlap with summary is acceptable).
|
|
3041
|
-
*/
|
|
3042
|
-
restoreFromEntries(entries) {
|
|
3043
|
-
const compactIdx = entries.findLastIndex(
|
|
3044
|
-
(e) => e.type === "system" /* System */ && e.subtype === "compact" /* Compact */
|
|
3045
|
-
);
|
|
3046
|
-
if (compactIdx === -1) {
|
|
3047
|
-
this.compactSummary = null;
|
|
3048
|
-
this.compactStartIdx = 0;
|
|
3049
|
-
return;
|
|
3050
|
-
}
|
|
3051
|
-
const summary = entries[compactIdx].content;
|
|
3052
|
-
let budget = RECENT_TOKEN_BUDGET;
|
|
3053
|
-
let cutIdx = 0;
|
|
3054
|
-
for (let i = compactIdx - 1; i >= 0; i--) {
|
|
3055
|
-
const tokens = estimateEntryTokens(entries[i]);
|
|
3056
|
-
budget -= tokens;
|
|
3057
|
-
if (budget <= 0) {
|
|
3058
|
-
cutIdx = i + 1;
|
|
3059
|
-
break;
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
this.compactSummary = summary;
|
|
3063
|
-
this.compactStartIdx = compactIdx - cutIdx;
|
|
3064
|
-
}
|
|
3065
3110
|
};
|
|
3066
|
-
function estimateEntryTokens(entry) {
|
|
3067
|
-
const content = entry.message?.content ?? entry.content ?? "";
|
|
3068
|
-
let tokens = estimateTokens(String(content));
|
|
3069
|
-
if (entry.toolCalls?.length) {
|
|
3070
|
-
for (const tc of entry.toolCalls) {
|
|
3071
|
-
tokens += estimateTokens(tc.arguments ?? "");
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
if (entry.toolResults?.length) {
|
|
3075
|
-
for (const tr of entry.toolResults) {
|
|
3076
|
-
tokens += estimateTokens(tr.output?.value ?? "");
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
return tokens;
|
|
3080
|
-
}
|
|
3081
3111
|
|
|
3082
3112
|
// src/services/agents/worker-state.ts
|
|
3083
3113
|
import { join as join7 } from "path";
|
|
@@ -3110,33 +3140,67 @@ var WorkerState = class extends BaseWayangState {
|
|
|
3110
3140
|
if (runtimeState) {
|
|
3111
3141
|
this.data.runtimeState = runtimeState;
|
|
3112
3142
|
}
|
|
3113
|
-
const
|
|
3114
|
-
|
|
3115
|
-
|
|
3143
|
+
const allEntries = this.jsonlFile.read();
|
|
3144
|
+
const compactIdx = allEntries.findLastIndex(isCompactMarker);
|
|
3145
|
+
if (compactIdx === -1) {
|
|
3146
|
+
if (allEntries.length > 0) {
|
|
3147
|
+
this.data.conversation = allEntries;
|
|
3148
|
+
}
|
|
3149
|
+
} else {
|
|
3150
|
+
let budget = RECENT_TOKEN_BUDGET;
|
|
3151
|
+
let cutIdx = 0;
|
|
3152
|
+
for (let i = compactIdx - 1; i >= 0; i--) {
|
|
3153
|
+
budget -= estimateEntryTokens(allEntries[i]);
|
|
3154
|
+
if (budget <= 0) {
|
|
3155
|
+
cutIdx = i + 1;
|
|
3156
|
+
break;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
this.data.conversation = allEntries.slice(cutIdx);
|
|
3116
3160
|
}
|
|
3117
3161
|
this.logger.info("WorkerState restored");
|
|
3118
3162
|
}
|
|
3119
3163
|
};
|
|
3120
3164
|
|
|
3121
3165
|
// src/services/agents/utils/build-assistant-entry.ts
|
|
3122
|
-
function buildAssistantEntry(event, sessionId, uuid) {
|
|
3166
|
+
function buildAssistantEntry(event, sessionId, uuid, extras) {
|
|
3123
3167
|
const ts = nowISO();
|
|
3124
3168
|
const text = event.text ?? "";
|
|
3125
3169
|
const reasoningText = event.reasoningText?.trim() || // Fallback: join per-part reasoning when reasoningText is not populated
|
|
3126
3170
|
(event.reasoning ?? []).map((r) => r.text).filter(Boolean).join("\n") || void 0;
|
|
3127
3171
|
const reasoning = reasoningText && reasoningText !== "[]" ? reasoningText : void 0;
|
|
3128
|
-
const
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3172
|
+
const lookupRecord = extras?.recordedToolCalls;
|
|
3173
|
+
const toolCalls = event.toolCalls?.length ? event.toolCalls.map((tc) => {
|
|
3174
|
+
const rec = lookupRecord?.(tc.toolCallId);
|
|
3175
|
+
return {
|
|
3176
|
+
toolCallId: tc.toolCallId,
|
|
3177
|
+
toolName: tc.toolName,
|
|
3178
|
+
arguments: (
|
|
3179
|
+
// SDK OnStepFinishEvent type incomplete
|
|
3180
|
+
typeof tc.args === "string" ? tc.args : JSON.stringify(tc.args ?? tc.input ?? {})
|
|
3181
|
+
),
|
|
3182
|
+
...rec?.taskId !== void 0 && { taskId: rec.taskId },
|
|
3183
|
+
...rec?.status !== void 0 && { status: rec.status },
|
|
3184
|
+
...rec?.startedAt !== void 0 && { startedAt: rec.startedAt },
|
|
3185
|
+
...rec?.endedAt !== void 0 && { endedAt: rec.endedAt },
|
|
3186
|
+
...rec?.errorType !== void 0 && { errorType: rec.errorType }
|
|
3187
|
+
};
|
|
3188
|
+
}) : [];
|
|
3136
3189
|
const results = [];
|
|
3190
|
+
const withRecordMeta = (r) => {
|
|
3191
|
+
const rec = lookupRecord?.(r.toolCallId);
|
|
3192
|
+
if (!rec) return r;
|
|
3193
|
+
return {
|
|
3194
|
+
...r,
|
|
3195
|
+
...rec.status !== void 0 && { status: rec.status },
|
|
3196
|
+
...rec.errorType !== void 0 && { errorType: rec.errorType },
|
|
3197
|
+
...rec.startedAt !== void 0 && { startedAt: rec.startedAt },
|
|
3198
|
+
...rec.endedAt !== void 0 && { endedAt: rec.endedAt }
|
|
3199
|
+
};
|
|
3200
|
+
};
|
|
3137
3201
|
if (event.toolResults?.length) {
|
|
3138
3202
|
results.push(
|
|
3139
|
-
...event.toolResults.map((tr) => ({
|
|
3203
|
+
...event.toolResults.map((tr) => withRecordMeta({
|
|
3140
3204
|
toolCallId: tr.toolCallId,
|
|
3141
3205
|
toolName: tr.toolName,
|
|
3142
3206
|
output: {
|
|
@@ -3153,20 +3217,49 @@ function buildAssistantEntry(event, sessionId, uuid) {
|
|
|
3153
3217
|
if (Array.isArray(content)) {
|
|
3154
3218
|
for (const part of content) {
|
|
3155
3219
|
if (part.type === "tool-error" && part.toolCallId) {
|
|
3156
|
-
results.push({
|
|
3220
|
+
results.push(withRecordMeta({
|
|
3157
3221
|
toolCallId: part.toolCallId,
|
|
3158
3222
|
toolName: part.toolName ?? "unknown",
|
|
3159
3223
|
output: {
|
|
3160
3224
|
type: "text",
|
|
3161
3225
|
value: part.error?.message ?? part.error ?? "Tool input validation failed"
|
|
3162
3226
|
},
|
|
3163
|
-
isError: true
|
|
3164
|
-
|
|
3227
|
+
isError: true,
|
|
3228
|
+
status: "failed",
|
|
3229
|
+
errorType: "ToolInputValidationError"
|
|
3230
|
+
}));
|
|
3165
3231
|
}
|
|
3166
3232
|
}
|
|
3167
3233
|
}
|
|
3168
3234
|
const toolResults = results;
|
|
3169
|
-
const
|
|
3235
|
+
const rawUsage = event.usage;
|
|
3236
|
+
const usage = rawUsage ? {
|
|
3237
|
+
inputTokens: rawUsage.inputTokens ?? 0,
|
|
3238
|
+
outputTokens: rawUsage.outputTokens ?? 0,
|
|
3239
|
+
...cachedInputOf(rawUsage) !== void 0 && { cachedInputTokens: cachedInputOf(rawUsage) },
|
|
3240
|
+
...reasoningTokensOf(rawUsage) !== void 0 && { reasoningTokens: reasoningTokensOf(rawUsage) },
|
|
3241
|
+
...rawUsage.totalTokens !== void 0 && { totalTokens: rawUsage.totalTokens }
|
|
3242
|
+
} : void 0;
|
|
3243
|
+
const modelInfo = event.model;
|
|
3244
|
+
const model = modelInfo && (modelInfo.provider !== void 0 || modelInfo.modelId !== void 0) || extras?.temperature !== void 0 || extras?.maxOutputTokens !== void 0 ? {
|
|
3245
|
+
...modelInfo?.provider !== void 0 && { provider: modelInfo.provider },
|
|
3246
|
+
...modelInfo?.modelId !== void 0 && { model: modelInfo.modelId },
|
|
3247
|
+
...extras?.temperature !== void 0 && { temperature: extras.temperature },
|
|
3248
|
+
...extras?.maxOutputTokens !== void 0 && { maxOutputTokens: extras.maxOutputTokens }
|
|
3249
|
+
} : void 0;
|
|
3250
|
+
const stepStartedAt = toIso(event.response?.timestamp) ?? extras?.startedAt;
|
|
3251
|
+
const endedAt = extras?.endedAt ?? ts;
|
|
3252
|
+
const durationMs = stepStartedAt !== void 0 ? Math.max(0, new Date(endedAt).getTime() - new Date(stepStartedAt).getTime()) : void 0;
|
|
3253
|
+
const firstTokenAt = extras?.firstTokenAt;
|
|
3254
|
+
const timing = stepStartedAt !== void 0 || firstTokenAt !== void 0 ? {
|
|
3255
|
+
...stepStartedAt !== void 0 && { startedAt: stepStartedAt },
|
|
3256
|
+
...firstTokenAt !== void 0 && { firstTokenAt },
|
|
3257
|
+
endedAt,
|
|
3258
|
+
...durationMs !== void 0 && { durationMs }
|
|
3259
|
+
} : { endedAt };
|
|
3260
|
+
const responseId = event.response?.id;
|
|
3261
|
+
const requestId = typeof responseId === "string" && responseId.length > 0 ? responseId : void 0;
|
|
3262
|
+
const stepNumber = event.stepNumber;
|
|
3170
3263
|
return {
|
|
3171
3264
|
type: "assistant" /* Assistant */,
|
|
3172
3265
|
uuid: uuid ?? generateId("step"),
|
|
@@ -3181,9 +3274,129 @@ function buildAssistantEntry(event, sessionId, uuid) {
|
|
|
3181
3274
|
},
|
|
3182
3275
|
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
3183
3276
|
toolResults: toolResults.length > 0 ? toolResults : void 0,
|
|
3184
|
-
finishReason: event.finishReason
|
|
3277
|
+
finishReason: event.finishReason,
|
|
3278
|
+
...typeof stepNumber === "number" && { stepIndex: stepNumber },
|
|
3279
|
+
...model !== void 0 && { model },
|
|
3280
|
+
...timing !== void 0 && { timing },
|
|
3281
|
+
...requestId !== void 0 && { requestId },
|
|
3282
|
+
...extras?.traceId !== void 0 && { traceId: extras.traceId },
|
|
3283
|
+
...extras?.spanId !== void 0 && { spanId: extras.spanId }
|
|
3185
3284
|
};
|
|
3186
3285
|
}
|
|
3286
|
+
function cachedInputOf(usage) {
|
|
3287
|
+
if (usage.cachedInputTokens !== void 0) return usage.cachedInputTokens;
|
|
3288
|
+
return usage.inputTokenDetails?.cacheReadTokens;
|
|
3289
|
+
}
|
|
3290
|
+
function reasoningTokensOf(usage) {
|
|
3291
|
+
if (usage.reasoningTokens !== void 0) return usage.reasoningTokens;
|
|
3292
|
+
return usage.outputTokenDetails?.reasoningTokens;
|
|
3293
|
+
}
|
|
3294
|
+
function toIso(value) {
|
|
3295
|
+
if (value instanceof Date) {
|
|
3296
|
+
return Number.isNaN(value.getTime()) ? void 0 : value.toISOString();
|
|
3297
|
+
}
|
|
3298
|
+
if (typeof value === "string" && value.length > 0) {
|
|
3299
|
+
const d = new Date(value);
|
|
3300
|
+
return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
|
|
3301
|
+
}
|
|
3302
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3303
|
+
return new Date(value).toISOString();
|
|
3304
|
+
}
|
|
3305
|
+
return void 0;
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// src/services/agents/utils/entry-envelope.ts
|
|
3309
|
+
function applyTurnEnvelope(entry, env) {
|
|
3310
|
+
if (!env) return entry;
|
|
3311
|
+
if (entry.turnId === void 0) entry.turnId = env.turnId;
|
|
3312
|
+
if (entry.rootTurnId === void 0 && env.rootTurnId !== void 0) entry.rootTurnId = env.rootTurnId;
|
|
3313
|
+
if (entry.causationId === void 0 && env.causationId !== void 0) entry.causationId = env.causationId;
|
|
3314
|
+
if (entry.actor === void 0 && env.actor !== void 0) entry.actor = env.actor;
|
|
3315
|
+
return entry;
|
|
3316
|
+
}
|
|
3317
|
+
function syncStepTimestamps(entry) {
|
|
3318
|
+
if (entry.type !== "assistant" /* Assistant */) return;
|
|
3319
|
+
if (entry.timing?.startedAt !== void 0) entry.startedAt = entry.timing.startedAt;
|
|
3320
|
+
if (entry.timing?.endedAt !== void 0) entry.endedAt = entry.timing.endedAt;
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
// src/services/agents/utils/tool-call-recorder.ts
|
|
3324
|
+
var ToolCallRecorder = class {
|
|
3325
|
+
/**
|
|
3326
|
+
* @param logger Optional logger — used for debug diagnostics (e.g. an
|
|
3327
|
+
* attachTask call that finds no live record). Absent in unit tests.
|
|
3328
|
+
*/
|
|
3329
|
+
constructor(logger) {
|
|
3330
|
+
this.logger = logger;
|
|
3331
|
+
}
|
|
3332
|
+
logger;
|
|
3333
|
+
records = /* @__PURE__ */ new Map();
|
|
3334
|
+
/** Clear records from previous runs. Call at the start of each agent run. */
|
|
3335
|
+
beginTurn() {
|
|
3336
|
+
this.records.clear();
|
|
3337
|
+
}
|
|
3338
|
+
start(toolCallId, toolName) {
|
|
3339
|
+
this.records.set(toolCallId, { toolCallId, toolName, status: "running", startedAt: nowISO() });
|
|
3340
|
+
}
|
|
3341
|
+
finish(toolCallId, status, errorType) {
|
|
3342
|
+
const rec = this.records.get(toolCallId);
|
|
3343
|
+
if (!rec) return;
|
|
3344
|
+
rec.status = status;
|
|
3345
|
+
rec.endedAt = nowISO();
|
|
3346
|
+
if (errorType) rec.errorType = errorType;
|
|
3347
|
+
}
|
|
3348
|
+
/**
|
|
3349
|
+
* Attach an async task dispatched by this call (e.g. add_task) and mark the
|
|
3350
|
+
* call as 'running': the tool returned, but the dispatched task is what
|
|
3351
|
+
* stays in flight. Terminal state arrives later via signal entries —
|
|
3352
|
+
* append-only storage means this record is never updated afterwards.
|
|
3353
|
+
*/
|
|
3354
|
+
attachTask(toolCallId, taskId) {
|
|
3355
|
+
const rec = this.records.get(toolCallId);
|
|
3356
|
+
if (!rec) {
|
|
3357
|
+
this.logger?.debug({ toolCallId, taskId }, "attachTask: no live tool-call record for toolCallId");
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
rec.taskId = taskId;
|
|
3361
|
+
rec.status = "running";
|
|
3362
|
+
rec.endedAt = nowISO();
|
|
3363
|
+
}
|
|
3364
|
+
/** Look up a live record (used by the assistant-entry builder). */
|
|
3365
|
+
lookup(toolCallId) {
|
|
3366
|
+
return this.records.get(toolCallId);
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
var TOOL_ERROR_PREFIX = "[ERROR]";
|
|
3370
|
+
function recordToolCalls(tools, recorder) {
|
|
3371
|
+
const wrapped = {};
|
|
3372
|
+
for (const [name, def] of Object.entries(tools)) {
|
|
3373
|
+
if (!def || typeof def.execute !== "function") {
|
|
3374
|
+
wrapped[name] = def;
|
|
3375
|
+
continue;
|
|
3376
|
+
}
|
|
3377
|
+
const original = def.execute;
|
|
3378
|
+
wrapped[name] = {
|
|
3379
|
+
...def,
|
|
3380
|
+
execute: async (args, options) => {
|
|
3381
|
+
const toolCallId = options?.toolCallId ?? `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
3382
|
+
recorder.start(toolCallId, name);
|
|
3383
|
+
try {
|
|
3384
|
+
const result = await original(args, options);
|
|
3385
|
+
if (typeof result === "string" && result.startsWith(TOOL_ERROR_PREFIX)) {
|
|
3386
|
+
recorder.finish(toolCallId, "failed", "ToolError");
|
|
3387
|
+
} else {
|
|
3388
|
+
recorder.finish(toolCallId, "succeeded");
|
|
3389
|
+
}
|
|
3390
|
+
return result;
|
|
3391
|
+
} catch (err) {
|
|
3392
|
+
recorder.finish(toolCallId, "failed", err?.constructor?.name ?? "Error");
|
|
3393
|
+
throw err;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
return wrapped;
|
|
3399
|
+
}
|
|
3187
3400
|
|
|
3188
3401
|
// src/services/agents/constants.ts
|
|
3189
3402
|
var WORKER_AGENT_MAX_STEP = 1e3;
|
|
@@ -3204,9 +3417,9 @@ function truncate(result) {
|
|
|
3204
3417
|
return result.slice(0, MAX_RESULT_CHARS) + "\n...(truncated)";
|
|
3205
3418
|
}
|
|
3206
3419
|
function safeExecute(name, fn) {
|
|
3207
|
-
return async (args,
|
|
3420
|
+
return async (args, options) => {
|
|
3208
3421
|
try {
|
|
3209
|
-
const result = await fn(args);
|
|
3422
|
+
const result = await fn(args, options);
|
|
3210
3423
|
return truncate(typeof result === "string" ? result : JSON.stringify(result));
|
|
3211
3424
|
} catch (err) {
|
|
3212
3425
|
return `[ERROR] ${name}: ${err.message ?? String(err)}`;
|
|
@@ -3762,10 +3975,14 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
3762
3975
|
_terminalResult = null;
|
|
3763
3976
|
workspaceDir;
|
|
3764
3977
|
toolChoice;
|
|
3978
|
+
provider;
|
|
3765
3979
|
contextManager;
|
|
3766
3980
|
ctx;
|
|
3981
|
+
/** Live tool execution records — merged into entries at step finish. */
|
|
3982
|
+
recorder = new ToolCallRecorder();
|
|
3767
3983
|
constructor(provider, ctx, skills, toolChoice) {
|
|
3768
3984
|
super(provider);
|
|
3985
|
+
this.provider = provider;
|
|
3769
3986
|
this.ctx = ctx;
|
|
3770
3987
|
this.logger = ctx.logger;
|
|
3771
3988
|
this.workspaceDir = ctx.workspaceDir;
|
|
@@ -3788,20 +4005,31 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
3788
4005
|
this.logger.debug({ task }, "WorkerAgent run called");
|
|
3789
4006
|
this.state.set("runtimeState.task", { id: task.id, description: task.description });
|
|
3790
4007
|
this._terminalResult = null;
|
|
3791
|
-
|
|
4008
|
+
const turnEnv = {
|
|
4009
|
+
turnId: task.executionTurnId ?? generateId("turn"),
|
|
4010
|
+
...(task.rootTurnId ?? task.turnId) !== void 0 && { rootTurnId: task.rootTurnId ?? task.turnId },
|
|
4011
|
+
causationId: task.id,
|
|
4012
|
+
actor: { kind: "worker", id: task.workerId, taskId: task.id }
|
|
4013
|
+
};
|
|
4014
|
+
this.recorder.beginTurn();
|
|
4015
|
+
const promptTs = nowISO();
|
|
4016
|
+
this.state.append("conversation", applyTurnEnvelope({
|
|
3792
4017
|
type: "user" /* User */,
|
|
3793
4018
|
uuid: generateId("u"),
|
|
3794
4019
|
parentUuid: null,
|
|
3795
|
-
sessionId: this.
|
|
3796
|
-
timestamp:
|
|
4020
|
+
sessionId: this.ctx.sessionId,
|
|
4021
|
+
timestamp: promptTs,
|
|
4022
|
+
actor: { kind: "system", id: "engine", taskId: task.id },
|
|
4023
|
+
// Delivery of the task prompt completes at write time.
|
|
4024
|
+
endedAt: promptTs,
|
|
3797
4025
|
message: { role: "user", content: task.description }
|
|
3798
|
-
});
|
|
3799
|
-
const allTools = {
|
|
4026
|
+
}, turnEnv));
|
|
4027
|
+
const allTools = recordToolCalls({
|
|
3800
4028
|
...tools,
|
|
3801
4029
|
check_controller_messages: checkControllerMessagesTool({
|
|
3802
4030
|
drainInbox: () => this.drainInbox()
|
|
3803
4031
|
})
|
|
3804
|
-
};
|
|
4032
|
+
}, this.recorder);
|
|
3805
4033
|
const result = await this.collectLoop({
|
|
3806
4034
|
system: this.contextManager.getSystemPrompt(),
|
|
3807
4035
|
messages: this.contextManager.getMessages(),
|
|
@@ -3812,8 +4040,16 @@ var WorkerAgent = class extends BaseAgent {
|
|
|
3812
4040
|
onStreamError: (err) => {
|
|
3813
4041
|
this.logger.error({ workerId: this.id, err }, "LLM stream error");
|
|
3814
4042
|
},
|
|
3815
|
-
onStep: (event) => {
|
|
3816
|
-
const entry = buildAssistantEntry(event, this.
|
|
4043
|
+
onStep: (event, meta) => {
|
|
4044
|
+
const entry = buildAssistantEntry(event, this.ctx.sessionId, void 0, {
|
|
4045
|
+
startedAt: meta?.startedAt,
|
|
4046
|
+
firstTokenAt: meta?.firstTokenAt,
|
|
4047
|
+
temperature: this.provider.temperature,
|
|
4048
|
+
maxOutputTokens: this.provider.maxOutputTokens,
|
|
4049
|
+
recordedToolCalls: (toolCallId) => this.recorder.lookup(toolCallId)
|
|
4050
|
+
});
|
|
4051
|
+
applyTurnEnvelope(entry, turnEnv);
|
|
4052
|
+
syncStepTimestamps(entry);
|
|
3817
4053
|
this.state.append("conversation", entry);
|
|
3818
4054
|
const toolNames = entry.toolCalls?.map((tc) => tc.toolName) ?? [];
|
|
3819
4055
|
const hasError = entry.toolResults?.some((tr) => tr.isError);
|
|
@@ -3932,6 +4168,23 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
3932
4168
|
// Signal context set during run()
|
|
3933
4169
|
_signalQueue = null;
|
|
3934
4170
|
_runContext = null;
|
|
4171
|
+
/** Turn lineage for permission-request signals emitted by this worker. */
|
|
4172
|
+
_turnLineage = {};
|
|
4173
|
+
/** Turn lineage stamped on entries written by the run in flight. */
|
|
4174
|
+
_currentTurnEnv = null;
|
|
4175
|
+
/**
|
|
4176
|
+
* Turn identity: one worker run = one turn. The engine passes the task's
|
|
4177
|
+
* execution turn id; rootTurnId is inherited from the task; the task is
|
|
4178
|
+
* the direct cause of this activation.
|
|
4179
|
+
*/
|
|
4180
|
+
buildTurnEnv(task) {
|
|
4181
|
+
return {
|
|
4182
|
+
turnId: task.executionTurnId ?? generateId("turn"),
|
|
4183
|
+
...(task.rootTurnId ?? task.turnId) !== void 0 && { rootTurnId: task.rootTurnId ?? task.turnId },
|
|
4184
|
+
causationId: task.id,
|
|
4185
|
+
actor: { kind: "worker", id: task.workerId, taskId: task.id }
|
|
4186
|
+
};
|
|
4187
|
+
}
|
|
3935
4188
|
/**
|
|
3936
4189
|
* Progress debounce mechanism.
|
|
3937
4190
|
*
|
|
@@ -3959,14 +4212,20 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
3959
4212
|
async run(task, _tools, onProgress) {
|
|
3960
4213
|
this._terminalResult = null;
|
|
3961
4214
|
this.state.set("runtimeState.task", { id: task.id, description: task.description });
|
|
3962
|
-
this.
|
|
4215
|
+
const turnEnv = this.buildTurnEnv(task);
|
|
4216
|
+
this._currentTurnEnv = turnEnv;
|
|
4217
|
+
const promptTs = nowISO();
|
|
4218
|
+
this.state.append("conversation", applyTurnEnvelope({
|
|
3963
4219
|
type: "user" /* User */,
|
|
3964
4220
|
uuid: generateId("u"),
|
|
3965
4221
|
parentUuid: null,
|
|
3966
|
-
sessionId: this.
|
|
3967
|
-
timestamp:
|
|
4222
|
+
sessionId: this.ctx.sessionId,
|
|
4223
|
+
timestamp: promptTs,
|
|
4224
|
+
actor: { kind: "system", id: "engine", taskId: task.id },
|
|
4225
|
+
// Delivery of the task prompt completes at write time.
|
|
4226
|
+
endedAt: promptTs,
|
|
3968
4227
|
message: { role: "user", content: task.description }
|
|
3969
|
-
});
|
|
4228
|
+
}, turnEnv));
|
|
3970
4229
|
try {
|
|
3971
4230
|
const options = {
|
|
3972
4231
|
cwd: this.workspaceDir,
|
|
@@ -4005,21 +4264,25 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
4005
4264
|
}
|
|
4006
4265
|
this.permissionHandler.cleanup();
|
|
4007
4266
|
this.flushMessageQueue();
|
|
4008
|
-
|
|
4267
|
+
const resultTs = nowISO();
|
|
4268
|
+
this.state.append("conversation", applyTurnEnvelope({
|
|
4009
4269
|
type: "system" /* System */,
|
|
4010
4270
|
uuid: generateId("s"),
|
|
4011
4271
|
parentUuid: null,
|
|
4012
|
-
sessionId: this.
|
|
4013
|
-
timestamp:
|
|
4014
|
-
subtype: this._terminalResult.status === "completed" ? "
|
|
4015
|
-
content: this._terminalResult.summary ?? this._terminalResult.error ?? ""
|
|
4016
|
-
|
|
4272
|
+
sessionId: this.ctx.sessionId,
|
|
4273
|
+
timestamp: resultTs,
|
|
4274
|
+
subtype: this._terminalResult.status === "completed" ? "task_result" /* TaskResult */ : "error" /* Error */,
|
|
4275
|
+
content: this._terminalResult.summary ?? this._terminalResult.error ?? "",
|
|
4276
|
+
startedAt: resultTs,
|
|
4277
|
+
endedAt: resultTs
|
|
4278
|
+
}, turnEnv));
|
|
4017
4279
|
return this._terminalResult;
|
|
4018
4280
|
}
|
|
4019
4281
|
/** Set signal context for permission requests. Called by engine before run(). */
|
|
4020
4282
|
setSignalContext(deps) {
|
|
4021
4283
|
this._signalQueue = deps.signalQueue;
|
|
4022
4284
|
this._runContext = deps;
|
|
4285
|
+
this._turnLineage = deps.turnLineage ?? {};
|
|
4023
4286
|
}
|
|
4024
4287
|
abort() {
|
|
4025
4288
|
this.abortController.abort();
|
|
@@ -4076,7 +4339,8 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
4076
4339
|
toolName,
|
|
4077
4340
|
toolArgs: input,
|
|
4078
4341
|
description
|
|
4079
|
-
}
|
|
4342
|
+
},
|
|
4343
|
+
...this._turnLineage
|
|
4080
4344
|
});
|
|
4081
4345
|
return new Promise((resolve11) => {
|
|
4082
4346
|
const timer = setTimeout(() => {
|
|
@@ -4150,21 +4414,24 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
|
|
|
4150
4414
|
}
|
|
4151
4415
|
const text = textParts.join("\n");
|
|
4152
4416
|
if (!text) return;
|
|
4153
|
-
this.state.append("conversation", {
|
|
4417
|
+
this.state.append("conversation", applyTurnEnvelope({
|
|
4154
4418
|
type: "assistant" /* Assistant */,
|
|
4155
4419
|
uuid: generateId("a"),
|
|
4156
4420
|
parentUuid: null,
|
|
4157
|
-
sessionId: this.
|
|
4421
|
+
sessionId: this.ctx.sessionId,
|
|
4158
4422
|
timestamp: nowISO(),
|
|
4159
4423
|
message: {
|
|
4160
4424
|
role: "assistant",
|
|
4161
4425
|
content: text,
|
|
4162
4426
|
usage: {
|
|
4163
4427
|
inputTokens: msg.message.usage?.input_tokens ?? 0,
|
|
4164
|
-
outputTokens: msg.message.usage?.output_tokens ?? 0
|
|
4428
|
+
outputTokens: msg.message.usage?.output_tokens ?? 0,
|
|
4429
|
+
...msg.message.usage?.cache_read_input_tokens != null && {
|
|
4430
|
+
cachedInputTokens: msg.message.usage.cache_read_input_tokens
|
|
4431
|
+
}
|
|
4165
4432
|
}
|
|
4166
4433
|
}
|
|
4167
|
-
});
|
|
4434
|
+
}, this._currentTurnEnv ?? void 0));
|
|
4168
4435
|
if (onProgress) {
|
|
4169
4436
|
this._pendingProgress = text.slice(0, 200);
|
|
4170
4437
|
if (this._progressTimer) clearTimeout(this._progressTimer);
|
|
@@ -4235,7 +4502,7 @@ function addTaskTool(deps) {
|
|
|
4235
4502
|
"Dispatch this task to an existing idle worker (the workerId returned from a prior multiStage task). The worker resumes with its accumulated context. Omit for a fresh worker."
|
|
4236
4503
|
)
|
|
4237
4504
|
}),
|
|
4238
|
-
execute: safeExecute("add_task", async ({ title, description, priority, workerType, multiStage, assignToWorker }) => {
|
|
4505
|
+
execute: safeExecute("add_task", async ({ title, description, priority, workerType, multiStage, assignToWorker }, options) => {
|
|
4239
4506
|
const resolvedType = workerType || "puppet";
|
|
4240
4507
|
if (deps.validateWorkerType) {
|
|
4241
4508
|
const err = deps.validateWorkerType(resolvedType);
|
|
@@ -4248,6 +4515,8 @@ function addTaskTool(deps) {
|
|
|
4248
4515
|
const err = deps.validateIdleWorker(assignToWorker);
|
|
4249
4516
|
if (err) return err;
|
|
4250
4517
|
}
|
|
4518
|
+
const toolCallId = options?.toolCallId;
|
|
4519
|
+
const turnCtx = deps.getTurnContext?.();
|
|
4251
4520
|
const task = {
|
|
4252
4521
|
id: generateId("t"),
|
|
4253
4522
|
title,
|
|
@@ -4257,14 +4526,19 @@ function addTaskTool(deps) {
|
|
|
4257
4526
|
status: "pending",
|
|
4258
4527
|
createdAt: Date.now(),
|
|
4259
4528
|
multiStage,
|
|
4260
|
-
assignToWorker
|
|
4529
|
+
assignToWorker,
|
|
4530
|
+
...turnCtx?.turnId !== void 0 && { turnId: turnCtx.turnId },
|
|
4531
|
+
...turnCtx?.rootTurnId !== void 0 && { rootTurnId: turnCtx.rootTurnId },
|
|
4532
|
+
...(toolCallId ?? turnCtx?.causationId) !== void 0 && { causationId: toolCallId ?? turnCtx?.causationId }
|
|
4261
4533
|
};
|
|
4262
4534
|
if (assignToWorker && deps.assignToWorker) {
|
|
4263
4535
|
const ok = deps.assignToWorker(assignToWorker, task);
|
|
4264
|
-
if (
|
|
4536
|
+
if (ok) deps.onTaskDispatched?.({ toolCallId, taskId: task.id });
|
|
4537
|
+
else return `Worker ${assignToWorker} could not accept the task (no longer idle?).`;
|
|
4265
4538
|
return `Task ${task.id} assigned to worker ${assignToWorker} (reusing its context, priority: ${priority}, worker: ${resolvedType}). You will be notified when the task completes.`;
|
|
4266
4539
|
}
|
|
4267
4540
|
deps.addTask(task);
|
|
4541
|
+
deps.onTaskDispatched?.({ toolCallId, taskId: task.id });
|
|
4268
4542
|
const stageNote = multiStage ? " [multi-stage: worker will idle after completion]" : "";
|
|
4269
4543
|
return `Task ${task.id} added (priority: ${priority}, worker: ${task.workerType})${stageNote}. You will be notified when the task completes.`;
|
|
4270
4544
|
})
|
|
@@ -4801,7 +5075,9 @@ function createControllerTools(deps) {
|
|
|
4801
5075
|
addTask: deps.addTask,
|
|
4802
5076
|
validateWorkerType: deps.validateWorkerType,
|
|
4803
5077
|
assignToWorker: deps.assignToWorker,
|
|
4804
|
-
validateIdleWorker: deps.validateIdleWorker
|
|
5078
|
+
validateIdleWorker: deps.validateIdleWorker,
|
|
5079
|
+
getTurnContext: deps.getTurnContext,
|
|
5080
|
+
onTaskDispatched: deps.onTaskDispatched
|
|
4805
5081
|
}),
|
|
4806
5082
|
list_tasks: listTasksTool({ listTasks: deps.listTasks }),
|
|
4807
5083
|
cancel_task: cancelTaskTool({
|
|
@@ -4877,7 +5153,8 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
|
|
|
4877
5153
|
toolName,
|
|
4878
5154
|
toolArgs: args,
|
|
4879
5155
|
description
|
|
4880
|
-
}
|
|
5156
|
+
},
|
|
5157
|
+
...deps.turnLineage ?? {}
|
|
4881
5158
|
});
|
|
4882
5159
|
const result = await new Promise((resolve11) => {
|
|
4883
5160
|
const timer = setTimeout(() => {
|
|
@@ -5035,6 +5312,19 @@ var TaskExecuteEngine = class {
|
|
|
5035
5312
|
workerTaskMap = /* @__PURE__ */ new Map();
|
|
5036
5313
|
/** Permission middleware resolvers keyed by workerId. */
|
|
5037
5314
|
permissionResolvers = /* @__PURE__ */ new Map();
|
|
5315
|
+
/**
|
|
5316
|
+
* Turn lineage of each active worker's current run: the execution turn id
|
|
5317
|
+
* (also handed to the worker for stamping its entries), the chain's root
|
|
5318
|
+
* turn, and the task id (used as signals' causationId).
|
|
5319
|
+
*/
|
|
5320
|
+
workerTurnLineage = /* @__PURE__ */ new Map();
|
|
5321
|
+
/**
|
|
5322
|
+
* Running tasks cancelled via cancel(): their (aborted) worker resolution
|
|
5323
|
+
* must not re-classify the task or emit a worker_failed signal — the
|
|
5324
|
+
* task_cancelled signal was already emitted at cancel time. Cleared when
|
|
5325
|
+
* the worker's run promise settles (handleDone/handleFail).
|
|
5326
|
+
*/
|
|
5327
|
+
cancelledTaskIds = /* @__PURE__ */ new Set();
|
|
5038
5328
|
/** Idle-reaping timers keyed by workerId (multi-stage workers only). */
|
|
5039
5329
|
idleTimers = /* @__PURE__ */ new Map();
|
|
5040
5330
|
/** Idle TTL (ms). Idle workers are disposed after this if not reused. */
|
|
@@ -5086,8 +5376,12 @@ var TaskExecuteEngine = class {
|
|
|
5086
5376
|
const running = this.taskState.get("tasks.running");
|
|
5087
5377
|
const runIdx = running.findIndex((t) => t.id === id);
|
|
5088
5378
|
if (runIdx !== -1) {
|
|
5379
|
+
const task = running[runIdx];
|
|
5380
|
+
const workerId = [...this.workerTaskMap.entries()].find(([, tId]) => tId === id)?.[0];
|
|
5089
5381
|
this.abortByTaskId(id);
|
|
5090
5382
|
this.moveToHistory(id, "cancelled", {});
|
|
5383
|
+
this.cancelledTaskIds.add(id);
|
|
5384
|
+
this.emitTaskCancelled(task, workerId);
|
|
5091
5385
|
this.logger.info({ taskId: id }, "Running task cancelled");
|
|
5092
5386
|
this.ctx.hooks.emit("task:cancelled", { taskId: id });
|
|
5093
5387
|
return true;
|
|
@@ -5102,6 +5396,7 @@ var TaskExecuteEngine = class {
|
|
|
5102
5396
|
status: "cancelled",
|
|
5103
5397
|
completedAt: Date.now()
|
|
5104
5398
|
});
|
|
5399
|
+
this.emitTaskCancelled(task);
|
|
5105
5400
|
this.logger.info({ taskId: id }, "Pending task cancelled");
|
|
5106
5401
|
this.ctx.hooks.emit("task:cancelled", { taskId: id });
|
|
5107
5402
|
return true;
|
|
@@ -5109,6 +5404,34 @@ var TaskExecuteEngine = class {
|
|
|
5109
5404
|
this.logger.warn({ taskId: id }, "Task not found for cancel");
|
|
5110
5405
|
return false;
|
|
5111
5406
|
}
|
|
5407
|
+
/**
|
|
5408
|
+
* Announce a task cancellation on the signal queue (subtype
|
|
5409
|
+
* task_cancelled — terminal for the dispatched task, but not a failure).
|
|
5410
|
+
* Running tasks carry their worker's lineage; pending tasks fall back to
|
|
5411
|
+
* the task's own chain fields.
|
|
5412
|
+
*/
|
|
5413
|
+
emitTaskCancelled(task, workerId) {
|
|
5414
|
+
const workerType = task.workerType ?? PUPPET_WORKER_TYPE;
|
|
5415
|
+
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
5416
|
+
const payload = {
|
|
5417
|
+
taskId: task.id,
|
|
5418
|
+
...workerId !== void 0 && { workerId },
|
|
5419
|
+
taskTitle: task.title,
|
|
5420
|
+
workerType,
|
|
5421
|
+
emoji: meta.emoji
|
|
5422
|
+
};
|
|
5423
|
+
if (workerId !== void 0) {
|
|
5424
|
+
this.enqueueWorkerSignal(workerId, { source: "system", type: "task_cancelled", payload });
|
|
5425
|
+
return;
|
|
5426
|
+
}
|
|
5427
|
+
this.signalQueue.enqueue({
|
|
5428
|
+
source: "system",
|
|
5429
|
+
type: "task_cancelled",
|
|
5430
|
+
payload,
|
|
5431
|
+
...(task.rootTurnId ?? task.turnId) !== void 0 && { rootTurnId: task.rootTurnId ?? task.turnId },
|
|
5432
|
+
causationId: task.id
|
|
5433
|
+
});
|
|
5434
|
+
}
|
|
5112
5435
|
updatePending(id, updates) {
|
|
5113
5436
|
const pending = [...this.taskState.get("tasks.pending")];
|
|
5114
5437
|
const idx = pending.findIndex((t) => t.id === id);
|
|
@@ -5179,6 +5502,39 @@ var TaskExecuteEngine = class {
|
|
|
5179
5502
|
return this.taskState.get("tasks.running").length;
|
|
5180
5503
|
}
|
|
5181
5504
|
// ---------------------------------------------------------------------------
|
|
5505
|
+
// Worker turn lineage
|
|
5506
|
+
// ---------------------------------------------------------------------------
|
|
5507
|
+
/**
|
|
5508
|
+
* Register the execution turn for a worker run and return it. The id is
|
|
5509
|
+
* handed to the worker (stamped on its entries) and reused on every signal
|
|
5510
|
+
* the engine emits for this worker, so the worker's conversation file and
|
|
5511
|
+
* the controller's signal records share the same turn id.
|
|
5512
|
+
*/
|
|
5513
|
+
registerWorkerTurn(workerId, task) {
|
|
5514
|
+
const executionTurnId = generateId("turn");
|
|
5515
|
+
this.workerTurnLineage.set(workerId, {
|
|
5516
|
+
turnId: executionTurnId,
|
|
5517
|
+
...task.rootTurnId !== void 0 && { rootTurnId: task.rootTurnId },
|
|
5518
|
+
...task.rootTurnId === void 0 && task.turnId !== void 0 && { rootTurnId: task.turnId },
|
|
5519
|
+
taskId: task.id
|
|
5520
|
+
});
|
|
5521
|
+
return executionTurnId;
|
|
5522
|
+
}
|
|
5523
|
+
/** Turn lineage to attach to signals emitted for a worker. */
|
|
5524
|
+
workerLineage(workerId) {
|
|
5525
|
+
const lin = this.workerTurnLineage.get(workerId);
|
|
5526
|
+
if (!lin) return {};
|
|
5527
|
+
return {
|
|
5528
|
+
turnId: lin.turnId,
|
|
5529
|
+
...lin.rootTurnId !== void 0 && { rootTurnId: lin.rootTurnId },
|
|
5530
|
+
causationId: lin.taskId
|
|
5531
|
+
};
|
|
5532
|
+
}
|
|
5533
|
+
/** Enqueue a worker signal with the worker's turn lineage attached. */
|
|
5534
|
+
enqueueWorkerSignal(workerId, sig) {
|
|
5535
|
+
this.signalQueue.enqueue({ ...sig, ...this.workerLineage(workerId) });
|
|
5536
|
+
}
|
|
5537
|
+
// ---------------------------------------------------------------------------
|
|
5182
5538
|
// Scheduling (direct calls, no hooks)
|
|
5183
5539
|
// ---------------------------------------------------------------------------
|
|
5184
5540
|
/** Attempt to schedule pending tasks up to maxConcurrency. */
|
|
@@ -5382,7 +5738,7 @@ var TaskExecuteEngine = class {
|
|
|
5382
5738
|
const worker = this.workers.get(workerId);
|
|
5383
5739
|
worker?.abort();
|
|
5384
5740
|
this.clearIdleTimer(workerId);
|
|
5385
|
-
this.
|
|
5741
|
+
this.enqueueWorkerSignal(workerId, {
|
|
5386
5742
|
source: "system",
|
|
5387
5743
|
type: "cancelled",
|
|
5388
5744
|
payload: {
|
|
@@ -5424,10 +5780,11 @@ var TaskExecuteEngine = class {
|
|
|
5424
5780
|
const worker = this.createWorker(workerType);
|
|
5425
5781
|
this.workers.set(workerId, worker);
|
|
5426
5782
|
this.workerTaskMap.set(workerId, task.id);
|
|
5783
|
+
const executionTurnId = this.registerWorkerTurn(workerId, task);
|
|
5427
5784
|
const onProgress = this.buildProgressEmitter(workerId, task, workerType);
|
|
5428
5785
|
const tools = this.wireWorker(worker, task, workerId, workerType);
|
|
5429
|
-
this.logger.info({ workerId, taskId: task.id, workerType }, "Worker starting");
|
|
5430
|
-
return worker.run({ ...task, workerId }, tools, onProgress);
|
|
5786
|
+
this.logger.info({ workerId, taskId: task.id, workerType, executionTurnId }, "Worker starting");
|
|
5787
|
+
return worker.run({ ...task, workerId, executionTurnId }, tools, onProgress);
|
|
5431
5788
|
}
|
|
5432
5789
|
/**
|
|
5433
5790
|
* Dispatch a follow-up task to an existing idle (multi-stage) worker.
|
|
@@ -5457,6 +5814,7 @@ var TaskExecuteEngine = class {
|
|
|
5457
5814
|
};
|
|
5458
5815
|
this.taskState.append("tasks.running", taskWithOrigin);
|
|
5459
5816
|
this.workerTaskMap.set(workerId, task.id);
|
|
5817
|
+
const executionTurnId = this.registerWorkerTurn(workerId, taskWithOrigin);
|
|
5460
5818
|
this.updateActiveWorker(workerId, {
|
|
5461
5819
|
status: "running",
|
|
5462
5820
|
taskId: task.id,
|
|
@@ -5469,8 +5827,8 @@ var TaskExecuteEngine = class {
|
|
|
5469
5827
|
});
|
|
5470
5828
|
const onProgress = this.buildProgressEmitter(workerId, task, workerType);
|
|
5471
5829
|
const tools = this.wireWorker(worker, task, workerId, workerType);
|
|
5472
|
-
this.logger.info({ workerId, taskId: task.id, workerType }, "Idle worker reused");
|
|
5473
|
-
worker.run({ ...taskWithOrigin, workerId }, tools, onProgress).then((result) => this.handleDone(workerId, taskWithOrigin, result)).catch((err2) => this.handleFail(workerId, taskWithOrigin, formatLlmError(err2)));
|
|
5830
|
+
this.logger.info({ workerId, taskId: task.id, workerType, executionTurnId }, "Idle worker reused");
|
|
5831
|
+
worker.run({ ...taskWithOrigin, workerId, executionTurnId }, tools, onProgress).then((result) => this.handleDone(workerId, taskWithOrigin, result)).catch((err2) => this.handleFail(workerId, taskWithOrigin, formatLlmError(err2)));
|
|
5474
5832
|
return true;
|
|
5475
5833
|
}
|
|
5476
5834
|
/**
|
|
@@ -5480,7 +5838,7 @@ var TaskExecuteEngine = class {
|
|
|
5480
5838
|
buildProgressEmitter(workerId, task, workerType) {
|
|
5481
5839
|
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
5482
5840
|
return (msg) => {
|
|
5483
|
-
this.
|
|
5841
|
+
this.enqueueWorkerSignal(workerId, {
|
|
5484
5842
|
source: "worker",
|
|
5485
5843
|
type: "progress",
|
|
5486
5844
|
payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: meta.emoji, message: msg }
|
|
@@ -5500,7 +5858,7 @@ var TaskExecuteEngine = class {
|
|
|
5500
5858
|
tavilyApiKey: this.ctx.config.tavilyApiKey,
|
|
5501
5859
|
registry: this.skills,
|
|
5502
5860
|
reportProgress: (msg, _percent) => {
|
|
5503
|
-
this.
|
|
5861
|
+
this.enqueueWorkerSignal(workerId, {
|
|
5504
5862
|
source: "worker",
|
|
5505
5863
|
type: "progress",
|
|
5506
5864
|
payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: workerMeta.emoji, message: msg }
|
|
@@ -5541,7 +5899,8 @@ var TaskExecuteEngine = class {
|
|
|
5541
5899
|
taskTitle: task.title,
|
|
5542
5900
|
workerType,
|
|
5543
5901
|
emoji: workerMeta.emoji,
|
|
5544
|
-
signalQueue: this.signalQueue
|
|
5902
|
+
signalQueue: this.signalQueue,
|
|
5903
|
+
turnLineage: this.workerLineage(workerId)
|
|
5545
5904
|
});
|
|
5546
5905
|
tools = middleware.wrappedTools;
|
|
5547
5906
|
this.permissionResolvers.set(workerId, middleware);
|
|
@@ -5553,18 +5912,25 @@ var TaskExecuteEngine = class {
|
|
|
5553
5912
|
taskId: task.id,
|
|
5554
5913
|
taskTitle: task.title,
|
|
5555
5914
|
workerType,
|
|
5556
|
-
emoji: workerMeta.emoji
|
|
5915
|
+
emoji: workerMeta.emoji,
|
|
5916
|
+
turnLineage: this.workerLineage(workerId)
|
|
5557
5917
|
});
|
|
5558
5918
|
this.permissionResolvers.set(workerId, worker.permissionHandler);
|
|
5559
5919
|
}
|
|
5560
5920
|
return tools;
|
|
5561
5921
|
}
|
|
5562
5922
|
handleDone(workerId, task, result) {
|
|
5923
|
+
if (this.consumeCancelled(task.id)) {
|
|
5924
|
+
this.removeActiveWorker(workerId);
|
|
5925
|
+
this.removeWorkerTracking(workerId);
|
|
5926
|
+
this.scheduleNext();
|
|
5927
|
+
return;
|
|
5928
|
+
}
|
|
5563
5929
|
const workerType = task.workerType ?? PUPPET_WORKER_TYPE;
|
|
5564
5930
|
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
5565
5931
|
if (result.status !== "completed") {
|
|
5566
5932
|
this.failTask(task.id, result.error ?? "Unknown error");
|
|
5567
|
-
this.
|
|
5933
|
+
this.enqueueWorkerSignal(workerId, {
|
|
5568
5934
|
source: "worker",
|
|
5569
5935
|
type: "failed",
|
|
5570
5936
|
payload: {
|
|
@@ -5583,7 +5949,7 @@ var TaskExecuteEngine = class {
|
|
|
5583
5949
|
}
|
|
5584
5950
|
if (task.multiStage) {
|
|
5585
5951
|
this.completeTask(task.id, result.summary ?? "");
|
|
5586
|
-
this.
|
|
5952
|
+
this.enqueueWorkerSignal(workerId, {
|
|
5587
5953
|
source: "worker",
|
|
5588
5954
|
type: "completed",
|
|
5589
5955
|
payload: {
|
|
@@ -5600,7 +5966,7 @@ var TaskExecuteEngine = class {
|
|
|
5600
5966
|
return;
|
|
5601
5967
|
}
|
|
5602
5968
|
this.completeTask(task.id, result.summary ?? "");
|
|
5603
|
-
this.
|
|
5969
|
+
this.enqueueWorkerSignal(workerId, {
|
|
5604
5970
|
source: "worker",
|
|
5605
5971
|
type: "completed",
|
|
5606
5972
|
payload: {
|
|
@@ -5617,10 +5983,16 @@ var TaskExecuteEngine = class {
|
|
|
5617
5983
|
this.scheduleNext();
|
|
5618
5984
|
}
|
|
5619
5985
|
handleFail(workerId, task, error) {
|
|
5986
|
+
if (this.consumeCancelled(task.id)) {
|
|
5987
|
+
this.removeActiveWorker(workerId);
|
|
5988
|
+
this.removeWorkerTracking(workerId);
|
|
5989
|
+
this.scheduleNext();
|
|
5990
|
+
return;
|
|
5991
|
+
}
|
|
5620
5992
|
const workerType = task.workerType ?? PUPPET_WORKER_TYPE;
|
|
5621
5993
|
const meta = getWorkerMeta(workerType, this.workerConfigs);
|
|
5622
5994
|
this.failTask(task.id, error);
|
|
5623
|
-
this.
|
|
5995
|
+
this.enqueueWorkerSignal(workerId, {
|
|
5624
5996
|
source: "worker",
|
|
5625
5997
|
type: "failed",
|
|
5626
5998
|
payload: {
|
|
@@ -5636,8 +6008,16 @@ var TaskExecuteEngine = class {
|
|
|
5636
6008
|
this.removeWorkerTracking(workerId);
|
|
5637
6009
|
this.scheduleNext();
|
|
5638
6010
|
}
|
|
6011
|
+
/** Whether the task was cancelled; clears the marker (one-shot). */
|
|
6012
|
+
consumeCancelled(taskId) {
|
|
6013
|
+
if (!this.cancelledTaskIds.has(taskId)) return false;
|
|
6014
|
+
this.cancelledTaskIds.delete(taskId);
|
|
6015
|
+
this.logger.info({ taskId }, "Worker resolution for cancelled task suppressed");
|
|
6016
|
+
return true;
|
|
6017
|
+
}
|
|
5639
6018
|
removeWorkerTracking(workerId) {
|
|
5640
6019
|
this.clearIdleTimer(workerId);
|
|
6020
|
+
this.workerTurnLineage.delete(workerId);
|
|
5641
6021
|
setTimeout(() => {
|
|
5642
6022
|
this.permissionResolvers.get(workerId)?.cleanup();
|
|
5643
6023
|
this.permissionResolvers.delete(workerId);
|
|
@@ -5874,10 +6254,223 @@ var SessionManager = class _SessionManager {
|
|
|
5874
6254
|
|
|
5875
6255
|
// src/services/agents/controller-state.ts
|
|
5876
6256
|
import { join as join10 } from "path";
|
|
6257
|
+
|
|
6258
|
+
// src/services/conversation/conversation-index.ts
|
|
6259
|
+
function emptyUsage() {
|
|
6260
|
+
return { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, totalTokens: 0 };
|
|
6261
|
+
}
|
|
6262
|
+
function newTurn(turnId) {
|
|
6263
|
+
const view = {
|
|
6264
|
+
turnId,
|
|
6265
|
+
status: "running",
|
|
6266
|
+
steps: [],
|
|
6267
|
+
signals: [],
|
|
6268
|
+
toolCalls: [],
|
|
6269
|
+
usage: emptyUsage(),
|
|
6270
|
+
entryIds: []
|
|
6271
|
+
};
|
|
6272
|
+
return { view, unfinishedTaskIds: /* @__PURE__ */ new Set(), hasSyncRunning: false, hasFailure: false, hasCancelled: false, toolCallById: /* @__PURE__ */ new Map() };
|
|
6273
|
+
}
|
|
6274
|
+
function trackTimeBounds(view, iso, pick) {
|
|
6275
|
+
if (iso === void 0) return;
|
|
6276
|
+
const t = Date.parse(iso);
|
|
6277
|
+
if (Number.isNaN(t)) return;
|
|
6278
|
+
const current = pick === "start" ? view.startedAt : view.endedAt;
|
|
6279
|
+
if (current === void 0) {
|
|
6280
|
+
if (pick === "start") view.startedAt = iso;
|
|
6281
|
+
else view.endedAt = iso;
|
|
6282
|
+
return;
|
|
6283
|
+
}
|
|
6284
|
+
const cur = Date.parse(current);
|
|
6285
|
+
if (Number.isNaN(cur)) return;
|
|
6286
|
+
if (pick === "start" ? t < cur : t > cur) {
|
|
6287
|
+
if (pick === "start") view.startedAt = iso;
|
|
6288
|
+
else view.endedAt = iso;
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
function terminalSignalForTask(subtype) {
|
|
6292
|
+
if (subtype === "worker_failed" /* WorkerFailed */) return "terminal-failed";
|
|
6293
|
+
if (subtype === "task_cancelled" /* TaskCancelled */) return "terminal-cancelled";
|
|
6294
|
+
if (subtype === "worker_completed" /* WorkerCompleted */ || subtype === "worker_disposed" /* WorkerDisposed */) return "terminal-ok";
|
|
6295
|
+
return null;
|
|
6296
|
+
}
|
|
6297
|
+
var ConversationIndexBuilder = class {
|
|
6298
|
+
accs = [];
|
|
6299
|
+
byTurnId = /* @__PURE__ */ new Map();
|
|
6300
|
+
byEntryIdMap = /* @__PURE__ */ new Map();
|
|
6301
|
+
/** taskId → turn that dispatched it (for terminal-signal resolution). */
|
|
6302
|
+
taskDispatchTurn = /* @__PURE__ */ new Map();
|
|
6303
|
+
/** Sequence for derived legacy turn ids. */
|
|
6304
|
+
legacySeq = 0;
|
|
6305
|
+
/** Turn id derived for the most recent entry lacking an explicit turnId. */
|
|
6306
|
+
currentDerivedTurnId;
|
|
6307
|
+
constructor(entries) {
|
|
6308
|
+
for (const entry of entries ?? []) this.onAppend(entry);
|
|
6309
|
+
}
|
|
6310
|
+
/**
|
|
6311
|
+
* Feed one more entry. O(1) amortized (a terminal signal also updates the
|
|
6312
|
+
* dispatching turn). Duplicate uuids are ignored, making append feeds
|
|
6313
|
+
* idempotent.
|
|
6314
|
+
*/
|
|
6315
|
+
onAppend(entry) {
|
|
6316
|
+
if (entry.uuid !== void 0 && this.byEntryIdMap.has(entry.uuid)) return;
|
|
6317
|
+
const turnId = this.resolveTurnId(entry);
|
|
6318
|
+
let acc = this.byTurnId.get(turnId);
|
|
6319
|
+
if (!acc) {
|
|
6320
|
+
acc = newTurn(turnId);
|
|
6321
|
+
this.accs.push(acc);
|
|
6322
|
+
this.byTurnId.set(turnId, acc);
|
|
6323
|
+
}
|
|
6324
|
+
const { view } = acc;
|
|
6325
|
+
this.byEntryIdMap.set(entry.uuid, view);
|
|
6326
|
+
view.entryIds.push(entry.uuid);
|
|
6327
|
+
trackTimeBounds(view, entry.startedAt ?? entry.timestamp, "start");
|
|
6328
|
+
trackTimeBounds(view, entry.endedAt ?? entry.timestamp, "end");
|
|
6329
|
+
if (view.rootTurnId === void 0 && entry.rootTurnId !== void 0) {
|
|
6330
|
+
view.rootTurnId = entry.rootTurnId;
|
|
6331
|
+
}
|
|
6332
|
+
if (entry.actor !== void 0) {
|
|
6333
|
+
if (acc.firstActor === void 0) acc.firstActor = entry.actor;
|
|
6334
|
+
if (acc.agentActor === void 0 && (entry.type === "assistant" /* Assistant */ || entry.type === "system" /* System */)) {
|
|
6335
|
+
acc.agentActor = entry.actor;
|
|
6336
|
+
}
|
|
6337
|
+
}
|
|
6338
|
+
view.actor = acc.agentActor ?? acc.firstActor;
|
|
6339
|
+
switch (entry.type) {
|
|
6340
|
+
case "user" /* User */:
|
|
6341
|
+
this.applyUserEntry(acc, entry);
|
|
6342
|
+
break;
|
|
6343
|
+
case "assistant" /* Assistant */:
|
|
6344
|
+
this.applyAssistantEntry(acc, entry);
|
|
6345
|
+
break;
|
|
6346
|
+
case "signal" /* Signal */:
|
|
6347
|
+
this.applySignalEntry(acc, entry);
|
|
6348
|
+
break;
|
|
6349
|
+
case "system" /* System */:
|
|
6350
|
+
this.applySystemEntry(acc, entry);
|
|
6351
|
+
break;
|
|
6352
|
+
}
|
|
6353
|
+
this.recomputeStatus(acc);
|
|
6354
|
+
}
|
|
6355
|
+
/** Read-only index snapshot (live objects — callers must not mutate). */
|
|
6356
|
+
getIndex() {
|
|
6357
|
+
return { turns: this.accs.map((a) => a.view), byEntryId: this.byEntryIdMap };
|
|
6358
|
+
}
|
|
6359
|
+
/** Look up a turn by id. */
|
|
6360
|
+
getTurn(turnId) {
|
|
6361
|
+
return this.byTurnId.get(turnId)?.view;
|
|
6362
|
+
}
|
|
6363
|
+
/** Look up the turn an entry belongs to. */
|
|
6364
|
+
getTurnByEntryId(entryId) {
|
|
6365
|
+
return this.byEntryIdMap.get(entryId);
|
|
6366
|
+
}
|
|
6367
|
+
get turnCount() {
|
|
6368
|
+
return this.accs.length;
|
|
6369
|
+
}
|
|
6370
|
+
// --- Internals ---
|
|
6371
|
+
/**
|
|
6372
|
+
* Resolve the turn an entry belongs to. New data carries `turnId`; legacy
|
|
6373
|
+
* data is backfilled: a user entry starts a new derived turn, anything else
|
|
6374
|
+
* attaches to the most recent turn.
|
|
6375
|
+
*/
|
|
6376
|
+
resolveTurnId(entry) {
|
|
6377
|
+
if (entry.turnId !== void 0 && entry.turnId !== null) {
|
|
6378
|
+
this.currentDerivedTurnId = entry.turnId;
|
|
6379
|
+
return entry.turnId;
|
|
6380
|
+
}
|
|
6381
|
+
if (entry.type === "user" /* User */ || this.currentDerivedTurnId === void 0) {
|
|
6382
|
+
this.currentDerivedTurnId = `legacy-turn-${this.legacySeq++}`;
|
|
6383
|
+
}
|
|
6384
|
+
return this.currentDerivedTurnId;
|
|
6385
|
+
}
|
|
6386
|
+
applyUserEntry(acc, entry) {
|
|
6387
|
+
if (acc.view.userEntry === void 0) acc.view.userEntry = entry;
|
|
6388
|
+
}
|
|
6389
|
+
applyAssistantEntry(acc, entry) {
|
|
6390
|
+
const { view } = acc;
|
|
6391
|
+
view.steps.push(entry);
|
|
6392
|
+
const u = entry.message?.usage;
|
|
6393
|
+
if (u) {
|
|
6394
|
+
view.usage.inputTokens += u.inputTokens ?? 0;
|
|
6395
|
+
view.usage.outputTokens += u.outputTokens ?? 0;
|
|
6396
|
+
view.usage.cachedInputTokens += u.cachedInputTokens ?? 0;
|
|
6397
|
+
view.usage.reasoningTokens += u.reasoningTokens ?? 0;
|
|
6398
|
+
view.usage.totalTokens += u.totalTokens ?? (u.inputTokens ?? 0) + (u.outputTokens ?? 0);
|
|
6399
|
+
}
|
|
6400
|
+
if (entry.toolCalls) {
|
|
6401
|
+
for (const tc of entry.toolCalls) {
|
|
6402
|
+
const vtc = {
|
|
6403
|
+
toolCallId: tc.toolCallId,
|
|
6404
|
+
toolName: tc.toolName,
|
|
6405
|
+
...tc.taskId !== void 0 && { taskId: tc.taskId },
|
|
6406
|
+
...tc.status !== void 0 && { status: tc.status },
|
|
6407
|
+
...tc.startedAt !== void 0 && { startedAt: tc.startedAt },
|
|
6408
|
+
...tc.endedAt !== void 0 && { endedAt: tc.endedAt }
|
|
6409
|
+
};
|
|
6410
|
+
view.toolCalls.push(vtc);
|
|
6411
|
+
acc.toolCallById.set(tc.toolCallId, vtc);
|
|
6412
|
+
if (tc.status === "failed") acc.hasFailure = true;
|
|
6413
|
+
if (tc.status === "pending" || tc.status === "running") {
|
|
6414
|
+
if (tc.taskId !== void 0) {
|
|
6415
|
+
acc.unfinishedTaskIds.add(tc.taskId);
|
|
6416
|
+
this.taskDispatchTurn.set(tc.taskId, acc);
|
|
6417
|
+
} else {
|
|
6418
|
+
acc.hasSyncRunning = true;
|
|
6419
|
+
}
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
if (entry.toolResults) {
|
|
6424
|
+
for (const tr of entry.toolResults) {
|
|
6425
|
+
const status = tr.status ?? (tr.isError ? "failed" : "succeeded");
|
|
6426
|
+
const vtc = acc.toolCallById.get(tr.toolCallId);
|
|
6427
|
+
if (vtc && status !== void 0) vtc.resultStatus = status;
|
|
6428
|
+
if (tr.isError || status === "failed") acc.hasFailure = true;
|
|
6429
|
+
}
|
|
6430
|
+
}
|
|
6431
|
+
}
|
|
6432
|
+
applySignalEntry(acc, entry) {
|
|
6433
|
+
const { view } = acc;
|
|
6434
|
+
view.signals.push(entry);
|
|
6435
|
+
const terminal = terminalSignalForTask(entry.subtype);
|
|
6436
|
+
if (terminal !== null && entry.taskId !== void 0) {
|
|
6437
|
+
if (terminal === "terminal-failed") acc.hasFailure = true;
|
|
6438
|
+
const dispatchAcc = this.taskDispatchTurn.get(entry.taskId);
|
|
6439
|
+
if (dispatchAcc) {
|
|
6440
|
+
dispatchAcc.unfinishedTaskIds.delete(entry.taskId);
|
|
6441
|
+
if (terminal === "terminal-failed") dispatchAcc.hasFailure = true;
|
|
6442
|
+
if (terminal === "terminal-cancelled") dispatchAcc.hasCancelled = true;
|
|
6443
|
+
this.recomputeStatus(dispatchAcc);
|
|
6444
|
+
}
|
|
6445
|
+
}
|
|
6446
|
+
}
|
|
6447
|
+
applySystemEntry(acc, entry) {
|
|
6448
|
+
if (!isCompactMarker(entry) && entry.subtype === "error" /* Error */) {
|
|
6449
|
+
acc.hasFailure = true;
|
|
6450
|
+
}
|
|
6451
|
+
}
|
|
6452
|
+
recomputeStatus(acc) {
|
|
6453
|
+
let status;
|
|
6454
|
+
if (acc.unfinishedTaskIds.size > 0 || acc.hasSyncRunning) {
|
|
6455
|
+
status = "running";
|
|
6456
|
+
} else if (acc.hasFailure) {
|
|
6457
|
+
status = "failed";
|
|
6458
|
+
} else if (acc.hasCancelled) {
|
|
6459
|
+
status = "cancelled";
|
|
6460
|
+
} else {
|
|
6461
|
+
status = "completed";
|
|
6462
|
+
}
|
|
6463
|
+
acc.view.status = status;
|
|
6464
|
+
}
|
|
6465
|
+
};
|
|
6466
|
+
|
|
6467
|
+
// src/services/agents/controller-state.ts
|
|
5877
6468
|
var ControllerAgentState = class extends BaseWayangState {
|
|
5878
6469
|
jsonFile;
|
|
5879
6470
|
jsonlFile;
|
|
5880
6471
|
logger;
|
|
6472
|
+
/** In-memory turn index — built in restore(), fed incrementally on append. */
|
|
6473
|
+
convIndex = null;
|
|
5881
6474
|
/** In-memory resolve function for the current inquiry — never persisted. */
|
|
5882
6475
|
inquiryResolver = null;
|
|
5883
6476
|
constructor(ctx) {
|
|
@@ -5891,7 +6484,7 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
5891
6484
|
},
|
|
5892
6485
|
conversation: [],
|
|
5893
6486
|
compactSummary: null,
|
|
5894
|
-
dynamicState: { streamingEntries: [], busy: false }
|
|
6487
|
+
dynamicState: { streamingEntries: [], busy: false, currentTurn: null }
|
|
5895
6488
|
};
|
|
5896
6489
|
super(initialData, [
|
|
5897
6490
|
{ path: "runtimeState", helper: jsonFile },
|
|
@@ -5901,36 +6494,59 @@ var ControllerAgentState = class extends BaseWayangState {
|
|
|
5901
6494
|
this.jsonlFile = jsonlFile;
|
|
5902
6495
|
this.logger = ctx.logger;
|
|
5903
6496
|
}
|
|
6497
|
+
/**
|
|
6498
|
+
* Append with turn-index feeding: mechanical envelope fields (v / parentId,
|
|
6499
|
+
* compaction-boundary aware) are stamped by BaseWayangState.append via the
|
|
6500
|
+
* shared conversation-envelope module — the single source of truth. This
|
|
6501
|
+
* override only feeds the in-memory turn index incrementally.
|
|
6502
|
+
*/
|
|
6503
|
+
append(path7, entry) {
|
|
6504
|
+
super.append(path7, entry);
|
|
6505
|
+
if (path7 === "conversation" && this.convIndex) {
|
|
6506
|
+
this.convIndex.onAppend(entry);
|
|
6507
|
+
}
|
|
6508
|
+
}
|
|
6509
|
+
/** Read the full persisted conversation (all JSONL lines, ignoring the in-memory window). */
|
|
6510
|
+
readFullConversation() {
|
|
6511
|
+
return this.jsonlFile.read();
|
|
6512
|
+
}
|
|
6513
|
+
/** Read-only turn index (live view; callers must not mutate). */
|
|
6514
|
+
getConversationIndex() {
|
|
6515
|
+
return this.convIndex?.getIndex() ?? { turns: [], byEntryId: /* @__PURE__ */ new Map() };
|
|
6516
|
+
}
|
|
6517
|
+
/** Look up one turn by id. */
|
|
6518
|
+
getTurn(turnId) {
|
|
6519
|
+
return this.convIndex?.getTurn(turnId);
|
|
6520
|
+
}
|
|
5904
6521
|
async restore() {
|
|
5905
6522
|
const runtimeState = this.jsonFile.read();
|
|
5906
6523
|
if (runtimeState) {
|
|
5907
6524
|
this.data.runtimeState = runtimeState;
|
|
5908
6525
|
}
|
|
5909
6526
|
const allEntries = this.jsonlFile.read();
|
|
5910
|
-
const compactIdx = allEntries.findLastIndex(
|
|
5911
|
-
(e) => isSystemEntry(e) && e.subtype === "compact" /* Compact */
|
|
5912
|
-
);
|
|
6527
|
+
const compactIdx = allEntries.findLastIndex(isCompactMarker);
|
|
5913
6528
|
if (compactIdx === -1) {
|
|
5914
6529
|
this.data.conversation = allEntries;
|
|
6530
|
+
this.data.compactSummary = null;
|
|
5915
6531
|
} else {
|
|
5916
|
-
|
|
5917
|
-
let budget = RECENT_TOKEN_BUDGET2;
|
|
6532
|
+
let budget = RECENT_TOKEN_BUDGET;
|
|
5918
6533
|
let cutIdx = 0;
|
|
5919
6534
|
for (let i = compactIdx - 1; i >= 0; i--) {
|
|
5920
|
-
|
|
5921
|
-
const content = getEntryContent(entry);
|
|
5922
|
-
budget -= Math.ceil(String(content).length / 4);
|
|
6535
|
+
budget -= estimateEntryTokens(allEntries[i]);
|
|
5923
6536
|
if (budget <= 0) {
|
|
5924
6537
|
cutIdx = i + 1;
|
|
5925
6538
|
break;
|
|
5926
6539
|
}
|
|
5927
6540
|
}
|
|
5928
6541
|
this.data.conversation = allEntries.slice(cutIdx);
|
|
6542
|
+
const marker = allEntries[compactIdx];
|
|
6543
|
+
this.data.compactSummary = marker.content;
|
|
5929
6544
|
}
|
|
5930
6545
|
this.logger.info({
|
|
5931
6546
|
entryCount: this.data.conversation.length,
|
|
5932
6547
|
totalEntries: allEntries.length
|
|
5933
6548
|
}, "ControllerAgentState restored");
|
|
6549
|
+
this.convIndex = new ConversationIndexBuilder(allEntries);
|
|
5934
6550
|
}
|
|
5935
6551
|
/** Set a pending inquiry and return a Promise that resolves when the user answers. */
|
|
5936
6552
|
askInquiry(question) {
|
|
@@ -5957,11 +6573,18 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
5957
6573
|
tools;
|
|
5958
6574
|
logger;
|
|
5959
6575
|
ctx;
|
|
5960
|
-
|
|
6576
|
+
provider;
|
|
6577
|
+
/** Live tool execution records — merged into entries at step finish. */
|
|
6578
|
+
recorder;
|
|
6579
|
+
/** Turn id of the most recent run — used to stamp late error entries. */
|
|
6580
|
+
lastTurnId = null;
|
|
6581
|
+
constructor(ctx, state, provider, tools, skills, recorder = new ToolCallRecorder()) {
|
|
5961
6582
|
super(provider);
|
|
5962
6583
|
this.ctx = ctx;
|
|
5963
6584
|
this.state = state;
|
|
5964
|
-
this.
|
|
6585
|
+
this.provider = provider;
|
|
6586
|
+
this.recorder = recorder;
|
|
6587
|
+
this.tools = recordToolCalls(tools, recorder);
|
|
5965
6588
|
this.logger = ctx.logger;
|
|
5966
6589
|
this.contextManager = new ContextManager(
|
|
5967
6590
|
state,
|
|
@@ -5984,6 +6607,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
5984
6607
|
static create(opts) {
|
|
5985
6608
|
const { ctx, provider, config, engine, signalQueue, skills } = opts;
|
|
5986
6609
|
const state = new ControllerAgentState(ctx);
|
|
6610
|
+
const recorder = new ToolCallRecorder(ctx.logger);
|
|
5987
6611
|
const tools = createControllerTools({
|
|
5988
6612
|
addTask: (task) => engine.add(task),
|
|
5989
6613
|
validateWorkerType: (type) => engine.validateWorkerType(type),
|
|
@@ -6006,9 +6630,16 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6006
6630
|
sendMessageToWorker: (workerId, message) => engine.sendMessageToWorker(workerId, message),
|
|
6007
6631
|
resolvePermission: (requestId, approved, reason) => engine.resolvePermission(requestId, approved, reason),
|
|
6008
6632
|
registry: skills,
|
|
6009
|
-
disposeWorker: (workerId) => engine.abortByWorkerId(workerId)
|
|
6633
|
+
disposeWorker: (workerId) => engine.abortByWorkerId(workerId),
|
|
6634
|
+
// Turn lineage for tasks created by add_task (read from dynamicState,
|
|
6635
|
+
// which run() sets at the start of every controller activation).
|
|
6636
|
+
getTurnContext: () => state.get("dynamicState.currentTurn") ?? void 0,
|
|
6637
|
+
// toolCall→task audit link, consumed when the step entry is built.
|
|
6638
|
+
onTaskDispatched: ({ toolCallId, taskId }) => {
|
|
6639
|
+
if (toolCallId) recorder.attachTask(toolCallId, taskId);
|
|
6640
|
+
}
|
|
6010
6641
|
});
|
|
6011
|
-
return new _ControllerAgent(ctx, state, provider, tools, skills);
|
|
6642
|
+
return new _ControllerAgent(ctx, state, provider, tools, skills, recorder);
|
|
6012
6643
|
}
|
|
6013
6644
|
/** Run the controller agent — streams text chunks while updating streamingEntries in state. */
|
|
6014
6645
|
async run(signals) {
|
|
@@ -6021,8 +6652,23 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6021
6652
|
}
|
|
6022
6653
|
async _run(signals) {
|
|
6023
6654
|
const ts = nowISO();
|
|
6655
|
+
const turnId = generateId("turn");
|
|
6656
|
+
const hasUserInput = signals.some((s) => s.type === "input");
|
|
6657
|
+
const rootTurnId = hasUserInput ? turnId : signals.find((s) => s.rootTurnId !== void 0)?.rootTurnId;
|
|
6658
|
+
const causationId = signals[0]?.id;
|
|
6659
|
+
const turnEnv = {
|
|
6660
|
+
turnId,
|
|
6661
|
+
...rootTurnId !== void 0 && { rootTurnId },
|
|
6662
|
+
...causationId !== void 0 && { causationId },
|
|
6663
|
+
actor: { kind: "controller", id: this.id }
|
|
6664
|
+
};
|
|
6665
|
+
this.state.set("dynamicState.currentTurn", turnEnv);
|
|
6666
|
+
this.lastTurnId = turnId;
|
|
6667
|
+
this.recorder.beginTurn();
|
|
6024
6668
|
for (const sig of signals) {
|
|
6025
|
-
|
|
6669
|
+
const entry = signalToEntry(sig, ts, this.ctx.sessionId);
|
|
6670
|
+
applyTurnEnvelope(entry, turnEnv);
|
|
6671
|
+
this.state.append("conversation", entry);
|
|
6026
6672
|
}
|
|
6027
6673
|
this.state.set("dynamicState.streamingEntries", []);
|
|
6028
6674
|
let streamingEntry = this.createStreamingEntry();
|
|
@@ -6035,10 +6681,18 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6035
6681
|
onStreamError: (err) => {
|
|
6036
6682
|
this.logger.error({ agent: "controller", err }, "LLM stream error");
|
|
6037
6683
|
},
|
|
6038
|
-
onStep: (event) => {
|
|
6684
|
+
onStep: (event, meta) => {
|
|
6039
6685
|
const streamingEntries = this.state.get("dynamicState.streamingEntries");
|
|
6040
6686
|
const streamingUuid = streamingEntries?.[0]?.uuid;
|
|
6041
|
-
const entry = buildAssistantEntry(event, this.
|
|
6687
|
+
const entry = buildAssistantEntry(event, this.ctx.sessionId, streamingUuid, {
|
|
6688
|
+
startedAt: meta?.startedAt,
|
|
6689
|
+
firstTokenAt: meta?.firstTokenAt,
|
|
6690
|
+
temperature: this.provider.temperature,
|
|
6691
|
+
maxOutputTokens: this.provider.maxOutputTokens,
|
|
6692
|
+
recordedToolCalls: (toolCallId) => this.recorder.lookup(toolCallId)
|
|
6693
|
+
});
|
|
6694
|
+
applyTurnEnvelope(entry, turnEnv);
|
|
6695
|
+
syncStepTimestamps(entry);
|
|
6042
6696
|
this.state.append("conversation", entry);
|
|
6043
6697
|
this.state.set("dynamicState.streamingEntries", []);
|
|
6044
6698
|
}
|
|
@@ -6063,7 +6717,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6063
6717
|
type: "assistant" /* Assistant */,
|
|
6064
6718
|
uuid: generateId("s"),
|
|
6065
6719
|
parentUuid: null,
|
|
6066
|
-
sessionId:
|
|
6720
|
+
sessionId: this.ctx.sessionId,
|
|
6067
6721
|
timestamp: nowISO(),
|
|
6068
6722
|
message: { role: "assistant", content: "" }
|
|
6069
6723
|
};
|
|
@@ -6071,6 +6725,10 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6071
6725
|
/** Restore persisted state (conversation, runtime). Called by Supervisor. */
|
|
6072
6726
|
async restore() {
|
|
6073
6727
|
await this.state.restore();
|
|
6728
|
+
const summary = this.state.get("compactSummary");
|
|
6729
|
+
if (summary) {
|
|
6730
|
+
this.contextManager.restoreCompact(summary);
|
|
6731
|
+
}
|
|
6074
6732
|
}
|
|
6075
6733
|
/** Initialize session metadata. Called by Supervisor at start. */
|
|
6076
6734
|
initSession(session) {
|
|
@@ -6083,15 +6741,29 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6083
6741
|
/** Surface an error in the UI as a system conversation entry. */
|
|
6084
6742
|
reportError(message) {
|
|
6085
6743
|
this.state.set("dynamicState.streamingEntries", []);
|
|
6086
|
-
|
|
6744
|
+
const errTs = nowISO();
|
|
6745
|
+
const entry = {
|
|
6087
6746
|
type: "system" /* System */,
|
|
6088
6747
|
uuid: generateId("err"),
|
|
6089
6748
|
parentUuid: null,
|
|
6090
|
-
sessionId:
|
|
6091
|
-
timestamp:
|
|
6749
|
+
sessionId: this.ctx.sessionId,
|
|
6750
|
+
timestamp: errTs,
|
|
6092
6751
|
subtype: "error" /* Error */,
|
|
6093
|
-
content: message
|
|
6094
|
-
|
|
6752
|
+
content: message,
|
|
6753
|
+
// Point-in-time event: startedAt/endedAt share the recording moment.
|
|
6754
|
+
startedAt: errTs,
|
|
6755
|
+
endedAt: errTs
|
|
6756
|
+
};
|
|
6757
|
+
applyTurnEnvelope(entry, this.lastTurnId !== null ? { turnId: this.lastTurnId, actor: { kind: "controller", id: this.id } } : void 0);
|
|
6758
|
+
this.state.append("conversation", entry);
|
|
6759
|
+
}
|
|
6760
|
+
/** Read-only turn index over the controller conversation. */
|
|
6761
|
+
getConversationIndex() {
|
|
6762
|
+
return this.state.getConversationIndex();
|
|
6763
|
+
}
|
|
6764
|
+
/** Look up one turn by id. */
|
|
6765
|
+
getTurn(turnId) {
|
|
6766
|
+
return this.state.getTurn(turnId);
|
|
6095
6767
|
}
|
|
6096
6768
|
// --- Subscribable ---
|
|
6097
6769
|
subscribe(path7, callback) {
|
|
@@ -6112,28 +6784,36 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6112
6784
|
*/
|
|
6113
6785
|
async performCompaction() {
|
|
6114
6786
|
try {
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
|
|
6125
|
-
// WayangModel opaque boundary
|
|
6126
|
-
system: SUMMARIZER_SYSTEM_PROMPT,
|
|
6127
|
-
prompt: buildSummarizerPrompt(entryTexts)
|
|
6128
|
-
});
|
|
6129
|
-
return text;
|
|
6130
|
-
} catch (err) {
|
|
6131
|
-
lastError = err;
|
|
6132
|
-
this.logger.warn({ attempt: attempt + 1, error: String(err) }, "Compaction summarizer failed");
|
|
6787
|
+
const previousSummary = this.contextManager.getCompactSummary();
|
|
6788
|
+
await this.contextManager.compact(
|
|
6789
|
+
async (entries) => {
|
|
6790
|
+
const entryTexts = entries.map((e, i) => {
|
|
6791
|
+
const content = e.message?.content ?? e.content ?? "";
|
|
6792
|
+
return `[${i}] (${e.type}) ${String(content).slice(0, 300)}`;
|
|
6793
|
+
});
|
|
6794
|
+
if (previousSummary) {
|
|
6795
|
+
entryTexts.unshift(`[Previous Summary]
|
|
6796
|
+
${previousSummary}`);
|
|
6133
6797
|
}
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6798
|
+
let lastError;
|
|
6799
|
+
for (let attempt = 0; attempt <= COMPACT_MAX_RETRIES; attempt++) {
|
|
6800
|
+
try {
|
|
6801
|
+
const { text } = await generateText({
|
|
6802
|
+
model: this.model,
|
|
6803
|
+
// WayangModel opaque boundary
|
|
6804
|
+
system: SUMMARIZER_SYSTEM_PROMPT,
|
|
6805
|
+
prompt: buildSummarizerPrompt(entryTexts)
|
|
6806
|
+
});
|
|
6807
|
+
return text;
|
|
6808
|
+
} catch (err) {
|
|
6809
|
+
lastError = err;
|
|
6810
|
+
this.logger.warn({ attempt: attempt + 1, error: String(err) }, "Compaction summarizer failed");
|
|
6811
|
+
}
|
|
6812
|
+
}
|
|
6813
|
+
throw lastError;
|
|
6814
|
+
},
|
|
6815
|
+
{ model: this.provider.modelName }
|
|
6816
|
+
);
|
|
6137
6817
|
} catch (err) {
|
|
6138
6818
|
this.logger.warn({ error: String(err) }, "Compaction failed, falling back to half-truncation");
|
|
6139
6819
|
const conversation = this.state.get("conversation");
|
|
@@ -6145,28 +6825,36 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
|
|
|
6145
6825
|
}
|
|
6146
6826
|
}
|
|
6147
6827
|
};
|
|
6148
|
-
function signalBase(ts) {
|
|
6828
|
+
function signalBase(ts, sessionId) {
|
|
6149
6829
|
return {
|
|
6150
6830
|
type: "signal" /* Signal */,
|
|
6151
6831
|
uuid: generateId("sig"),
|
|
6152
6832
|
parentUuid: null,
|
|
6153
|
-
sessionId
|
|
6833
|
+
sessionId,
|
|
6154
6834
|
timestamp: ts
|
|
6155
6835
|
};
|
|
6156
6836
|
}
|
|
6837
|
+
function isoMs(ms) {
|
|
6838
|
+
if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
|
|
6839
|
+
return new Date(ms).toISOString();
|
|
6840
|
+
}
|
|
6157
6841
|
var signalConverters = {
|
|
6158
|
-
input: (sig, ts) => ({
|
|
6842
|
+
input: (sig, ts, sessionId) => ({
|
|
6159
6843
|
type: "user" /* User */,
|
|
6160
6844
|
uuid: generateId("u"),
|
|
6161
6845
|
parentUuid: null,
|
|
6162
|
-
sessionId
|
|
6846
|
+
sessionId,
|
|
6163
6847
|
timestamp: ts,
|
|
6848
|
+
actor: { kind: "user", id: "user" },
|
|
6849
|
+
startedAt: isoMs(sig.timestamp),
|
|
6850
|
+
// Delivery/processing of the input is complete when it is recorded here.
|
|
6851
|
+
endedAt: ts,
|
|
6164
6852
|
message: { role: "user", content: sig.payload.text }
|
|
6165
6853
|
}),
|
|
6166
|
-
completed: (sig, ts) => {
|
|
6854
|
+
completed: (sig, ts, sessionId) => {
|
|
6167
6855
|
const p = sig.payload;
|
|
6168
6856
|
return {
|
|
6169
|
-
...signalBase(ts),
|
|
6857
|
+
...signalBase(ts, sessionId),
|
|
6170
6858
|
subtype: "worker_completed" /* WorkerCompleted */,
|
|
6171
6859
|
workerId: p.workerId,
|
|
6172
6860
|
workerType: p.workerType,
|
|
@@ -6176,10 +6864,10 @@ var signalConverters = {
|
|
|
6176
6864
|
content: p.summary ?? JSON.stringify(sig.payload)
|
|
6177
6865
|
};
|
|
6178
6866
|
},
|
|
6179
|
-
failed: (sig, ts) => {
|
|
6867
|
+
failed: (sig, ts, sessionId) => {
|
|
6180
6868
|
const p = sig.payload;
|
|
6181
6869
|
return {
|
|
6182
|
-
...signalBase(ts),
|
|
6870
|
+
...signalBase(ts, sessionId),
|
|
6183
6871
|
subtype: "worker_failed" /* WorkerFailed */,
|
|
6184
6872
|
workerId: p.workerId,
|
|
6185
6873
|
workerType: p.workerType,
|
|
@@ -6189,10 +6877,10 @@ var signalConverters = {
|
|
|
6189
6877
|
content: p.error
|
|
6190
6878
|
};
|
|
6191
6879
|
},
|
|
6192
|
-
progress: (sig, ts) => {
|
|
6880
|
+
progress: (sig, ts, sessionId) => {
|
|
6193
6881
|
const p = sig.payload;
|
|
6194
6882
|
return {
|
|
6195
|
-
...signalBase(ts),
|
|
6883
|
+
...signalBase(ts, sessionId),
|
|
6196
6884
|
subtype: "worker_progress" /* WorkerProgress */,
|
|
6197
6885
|
workerId: p.workerId,
|
|
6198
6886
|
workerType: p.workerType,
|
|
@@ -6202,11 +6890,11 @@ var signalConverters = {
|
|
|
6202
6890
|
content: p.message
|
|
6203
6891
|
};
|
|
6204
6892
|
},
|
|
6205
|
-
heartbeat: (sig, ts) => {
|
|
6893
|
+
heartbeat: (sig, ts, sessionId) => {
|
|
6206
6894
|
const p = sig.payload;
|
|
6207
6895
|
const workerSummary = p.workers.map((w) => `${w.taskTitle} (${w.workerType}, running ${Math.round(w.runningForMs / 1e3)}s)`).join(", ");
|
|
6208
6896
|
return {
|
|
6209
|
-
...signalBase(ts),
|
|
6897
|
+
...signalBase(ts, sessionId),
|
|
6210
6898
|
subtype: "heartbeat" /* Heartbeat */,
|
|
6211
6899
|
taskTitle: "heartbeat",
|
|
6212
6900
|
content: `[HEARTBEAT] ${p.reason}
|
|
@@ -6215,10 +6903,10 @@ Workers: ${workerSummary}
|
|
|
6215
6903
|
Pending tasks: ${p.pendingTaskCount}`
|
|
6216
6904
|
};
|
|
6217
6905
|
},
|
|
6218
|
-
permission_request: (sig, ts) => {
|
|
6906
|
+
permission_request: (sig, ts, sessionId) => {
|
|
6219
6907
|
const p = sig.payload;
|
|
6220
6908
|
return {
|
|
6221
|
-
...signalBase(ts),
|
|
6909
|
+
...signalBase(ts, sessionId),
|
|
6222
6910
|
subtype: "permission_request" /* PermissionRequest */,
|
|
6223
6911
|
workerId: p.workerId,
|
|
6224
6912
|
workerType: p.workerType,
|
|
@@ -6231,10 +6919,10 @@ Request ID: ${p.requestId}
|
|
|
6231
6919
|
Use respond_permission to approve or deny.`
|
|
6232
6920
|
};
|
|
6233
6921
|
},
|
|
6234
|
-
cancelled: (sig, ts) => {
|
|
6922
|
+
cancelled: (sig, ts, sessionId) => {
|
|
6235
6923
|
const p = sig.payload;
|
|
6236
6924
|
return {
|
|
6237
|
-
...signalBase(ts),
|
|
6925
|
+
...signalBase(ts, sessionId),
|
|
6238
6926
|
subtype: "worker_disposed" /* WorkerDisposed */,
|
|
6239
6927
|
workerId: p.workerId,
|
|
6240
6928
|
workerType: p.workerType,
|
|
@@ -6244,11 +6932,24 @@ Use respond_permission to approve or deny.`
|
|
|
6244
6932
|
content: `[WORKER DISPOSED] Idle (multi-stage) worker ${p.workerId} was disposed (reaped by timeout or manually). No task failed \u2014 its last stage already completed. If you still need this worker's context, create a fresh multi-stage task instead.`
|
|
6245
6933
|
};
|
|
6246
6934
|
},
|
|
6247
|
-
|
|
6935
|
+
task_cancelled: (sig, ts, sessionId) => {
|
|
6936
|
+
const p = sig.payload;
|
|
6937
|
+
return {
|
|
6938
|
+
...signalBase(ts, sessionId),
|
|
6939
|
+
subtype: "task_cancelled" /* TaskCancelled */,
|
|
6940
|
+
workerId: p.workerId,
|
|
6941
|
+
workerType: p.workerType,
|
|
6942
|
+
emoji: p.emoji,
|
|
6943
|
+
taskId: p.taskId,
|
|
6944
|
+
taskTitle: p.taskTitle ?? "task_cancelled",
|
|
6945
|
+
content: `[TASK CANCELLED] Task ${p.taskId}${p.taskTitle ? ` (${p.taskTitle})` : ""} was cancelled by the user/controller. This is not a failure \u2014 the task terminated as cancelled.`
|
|
6946
|
+
};
|
|
6947
|
+
},
|
|
6948
|
+
previous_session_tasks: (sig, ts, sessionId) => {
|
|
6248
6949
|
const p = sig.payload;
|
|
6249
6950
|
const lines = p.tasks.map((t) => `- [${t.status}] ${t.title} (id: ${t.taskId}): ${t.description}`);
|
|
6250
6951
|
return {
|
|
6251
|
-
...signalBase(ts),
|
|
6952
|
+
...signalBase(ts, sessionId),
|
|
6252
6953
|
subtype: "previous_session_tasks" /* PreviousSessionTasks */,
|
|
6253
6954
|
taskTitle: "previous_session_tasks",
|
|
6254
6955
|
content: `[PREVIOUS SESSION] ${p.tasks.length} unfinished task(s) from session ${p.sessionId} (last active ${new Date(p.lastActiveAt).toLocaleString()}):
|
|
@@ -6257,20 +6958,43 @@ Workers were NOT resumed. Decide: re-dispatch with add_task, treat as stale, or
|
|
|
6257
6958
|
};
|
|
6258
6959
|
}
|
|
6259
6960
|
};
|
|
6260
|
-
function signalToEntry(sig, ts) {
|
|
6961
|
+
function signalToEntry(sig, ts, sessionId) {
|
|
6261
6962
|
const converter = signalConverters[sig.type];
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6963
|
+
let entry;
|
|
6964
|
+
if (converter) {
|
|
6965
|
+
entry = converter(sig, ts, sessionId);
|
|
6966
|
+
} else {
|
|
6967
|
+
const p = sig.payload;
|
|
6968
|
+
entry = {
|
|
6969
|
+
...signalBase(ts, sessionId),
|
|
6970
|
+
subtype: "worker_failed" /* WorkerFailed */,
|
|
6971
|
+
workerId: p.workerId,
|
|
6972
|
+
workerType: p.workerType,
|
|
6973
|
+
emoji: p.emoji,
|
|
6974
|
+
taskId: p.taskId,
|
|
6975
|
+
taskTitle: p.taskTitle ?? sig.type,
|
|
6976
|
+
content: JSON.stringify(sig.payload)
|
|
6977
|
+
};
|
|
6978
|
+
}
|
|
6979
|
+
if (entry.type === "signal" /* Signal */) {
|
|
6980
|
+
const s = entry;
|
|
6981
|
+
s.signalId = sig.id;
|
|
6982
|
+
if (sig.rootTurnId !== void 0) s.rootTurnId = sig.rootTurnId;
|
|
6983
|
+
if (sig.causationId !== void 0) s.causationId = sig.causationId;
|
|
6984
|
+
s.startedAt = isoMs(sig.timestamp);
|
|
6985
|
+
s.endedAt = ts;
|
|
6986
|
+
const payload = sig.payload;
|
|
6987
|
+
if (sig.source === "worker" && typeof payload.workerId === "string") {
|
|
6988
|
+
s.actor = {
|
|
6989
|
+
kind: "worker",
|
|
6990
|
+
id: payload.workerId,
|
|
6991
|
+
...typeof payload.taskId === "string" && { taskId: payload.taskId }
|
|
6992
|
+
};
|
|
6993
|
+
} else if (sig.source === "system") {
|
|
6994
|
+
s.actor = { kind: "system", id: "system" };
|
|
6995
|
+
}
|
|
6996
|
+
}
|
|
6997
|
+
return entry;
|
|
6274
6998
|
}
|
|
6275
6999
|
|
|
6276
7000
|
// src/services/skills/registry.ts
|
|
@@ -6545,6 +7269,15 @@ var Supervisor = class {
|
|
|
6545
7269
|
resolveInquiry(answer) {
|
|
6546
7270
|
this.controllerAgent.resolveInquiry(answer);
|
|
6547
7271
|
}
|
|
7272
|
+
// --- Read-only conversation index (WebUI / audit) ---
|
|
7273
|
+
/** Grouped turn view over the controller conversation. */
|
|
7274
|
+
getConversationIndex() {
|
|
7275
|
+
return this.controllerAgent.getConversationIndex();
|
|
7276
|
+
}
|
|
7277
|
+
/** Look up one turn of the controller conversation by id. */
|
|
7278
|
+
getTurn(turnId) {
|
|
7279
|
+
return this.controllerAgent.getTurn(turnId);
|
|
7280
|
+
}
|
|
6548
7281
|
// --- Shutdown ---
|
|
6549
7282
|
async shutdown() {
|
|
6550
7283
|
this.controllerLoop.shutdown();
|
|
@@ -6591,6 +7324,96 @@ var RpcServer = class {
|
|
|
6591
7324
|
}
|
|
6592
7325
|
};
|
|
6593
7326
|
|
|
7327
|
+
// src/services/conversation/turn-serialize.ts
|
|
7328
|
+
var DEFAULT_TURN_TEXT_LIMIT = 2e3;
|
|
7329
|
+
function cut(text, limit) {
|
|
7330
|
+
if (text === void 0) return { value: "", truncated: false };
|
|
7331
|
+
if (text.length <= limit) return { value: text, truncated: false };
|
|
7332
|
+
return { value: `${text.slice(0, limit)}\u2026[truncated]`, truncated: true };
|
|
7333
|
+
}
|
|
7334
|
+
function serializeTurnView(view, limits = {}) {
|
|
7335
|
+
const textLimit = limits.maxTextLength ?? DEFAULT_TURN_TEXT_LIMIT;
|
|
7336
|
+
return {
|
|
7337
|
+
turnId: view.turnId,
|
|
7338
|
+
...view.rootTurnId !== void 0 && { rootTurnId: view.rootTurnId },
|
|
7339
|
+
...view.actor !== void 0 && { actor: view.actor },
|
|
7340
|
+
...view.startedAt !== void 0 && { startedAt: view.startedAt },
|
|
7341
|
+
...view.endedAt !== void 0 && { endedAt: view.endedAt },
|
|
7342
|
+
status: view.status,
|
|
7343
|
+
...view.userEntry !== void 0 && (() => {
|
|
7344
|
+
const u = cut(view.userEntry.message?.content, textLimit);
|
|
7345
|
+
return {
|
|
7346
|
+
userEntry: {
|
|
7347
|
+
uuid: view.userEntry.uuid,
|
|
7348
|
+
timestamp: view.userEntry.timestamp,
|
|
7349
|
+
content: u.value,
|
|
7350
|
+
contentTruncated: u.truncated
|
|
7351
|
+
}
|
|
7352
|
+
};
|
|
7353
|
+
})(),
|
|
7354
|
+
steps: view.steps.map((step) => {
|
|
7355
|
+
const content = cut(step.message?.content, textLimit);
|
|
7356
|
+
const reasoning = step.message?.reasoning !== void 0 ? cut(step.message.reasoning, textLimit) : void 0;
|
|
7357
|
+
return {
|
|
7358
|
+
uuid: step.uuid,
|
|
7359
|
+
...step.stepIndex !== void 0 && { stepIndex: step.stepIndex },
|
|
7360
|
+
timestamp: step.timestamp,
|
|
7361
|
+
content: content.value,
|
|
7362
|
+
contentTruncated: content.truncated,
|
|
7363
|
+
...reasoning !== void 0 && {
|
|
7364
|
+
reasoning: reasoning.value,
|
|
7365
|
+
reasoningTruncated: reasoning.truncated
|
|
7366
|
+
},
|
|
7367
|
+
...step.finishReason !== void 0 && { finishReason: step.finishReason },
|
|
7368
|
+
...step.model !== void 0 && { model: step.model },
|
|
7369
|
+
...step.timing !== void 0 && { timing: step.timing },
|
|
7370
|
+
...step.requestId !== void 0 && { requestId: step.requestId },
|
|
7371
|
+
...step.message?.usage !== void 0 && { usage: step.message.usage },
|
|
7372
|
+
toolCallCount: step.toolCalls?.length ?? 0,
|
|
7373
|
+
toolResultCount: step.toolResults?.length ?? 0
|
|
7374
|
+
};
|
|
7375
|
+
}),
|
|
7376
|
+
signals: view.signals.map((sig) => {
|
|
7377
|
+
const c = cut(sig.content, textLimit);
|
|
7378
|
+
return {
|
|
7379
|
+
uuid: sig.uuid,
|
|
7380
|
+
subtype: sig.subtype,
|
|
7381
|
+
...sig.signalId !== void 0 && { signalId: sig.signalId },
|
|
7382
|
+
...sig.workerId !== void 0 && { workerId: sig.workerId },
|
|
7383
|
+
...sig.taskId !== void 0 && { taskId: sig.taskId },
|
|
7384
|
+
taskTitle: sig.taskTitle,
|
|
7385
|
+
timestamp: sig.timestamp,
|
|
7386
|
+
content: c.value,
|
|
7387
|
+
contentTruncated: c.truncated
|
|
7388
|
+
};
|
|
7389
|
+
}),
|
|
7390
|
+
toolCalls: view.toolCalls,
|
|
7391
|
+
usage: view.usage,
|
|
7392
|
+
entryIds: view.entryIds
|
|
7393
|
+
};
|
|
7394
|
+
}
|
|
7395
|
+
function summarizeTurn(view, limits = {}) {
|
|
7396
|
+
const previewLimit = Math.min(limits.maxTextLength ?? 200, 200);
|
|
7397
|
+
const preview = view.userEntry !== void 0 ? cut(view.userEntry.message?.content, previewLimit) : void 0;
|
|
7398
|
+
return {
|
|
7399
|
+
turnId: view.turnId,
|
|
7400
|
+
...view.rootTurnId !== void 0 && { rootTurnId: view.rootTurnId },
|
|
7401
|
+
...view.actor !== void 0 && { actor: view.actor },
|
|
7402
|
+
...view.startedAt !== void 0 && { startedAt: view.startedAt },
|
|
7403
|
+
...view.endedAt !== void 0 && { endedAt: view.endedAt },
|
|
7404
|
+
status: view.status,
|
|
7405
|
+
usage: view.usage,
|
|
7406
|
+
stepCount: view.steps.length,
|
|
7407
|
+
signalCount: view.signals.length,
|
|
7408
|
+
toolCallCount: view.toolCalls.length,
|
|
7409
|
+
entryCount: view.entryIds.length,
|
|
7410
|
+
...preview !== void 0 && {
|
|
7411
|
+
userPreview: preview.value,
|
|
7412
|
+
userPreviewTruncated: preview.truncated
|
|
7413
|
+
}
|
|
7414
|
+
};
|
|
7415
|
+
}
|
|
7416
|
+
|
|
6594
7417
|
// src/server/wayang-server.ts
|
|
6595
7418
|
async function runWayangServer(options, io = process) {
|
|
6596
7419
|
const config = await loadConfig(options.configPath, { nonInteractive: true });
|
|
@@ -6703,6 +7526,24 @@ async function runWayangServer(options, io = process) {
|
|
|
6703
7526
|
const data = target.getSnapshot(requiredString(params, "path"));
|
|
6704
7527
|
return { data: data === void 0 ? null : data };
|
|
6705
7528
|
});
|
|
7529
|
+
rpc.register(METHOD.CONVERSATION_LIST_TURNS, (params) => {
|
|
7530
|
+
const index = supervisor.getConversationIndex();
|
|
7531
|
+
const limit = optionalInt(params, "limit", 50, 1, 500);
|
|
7532
|
+
const offset = optionalInt(params, "offset", 0, 0, Number.MAX_SAFE_INTEGER);
|
|
7533
|
+
const turns = index.turns.slice(offset, offset + limit).map((view) => summarizeTurn(view, { maxTextLength: optionalInt(params, "previewLength", 200, 1, 2e3) }));
|
|
7534
|
+
return { total: index.turns.length, offset, limit, turns };
|
|
7535
|
+
});
|
|
7536
|
+
rpc.register(METHOD.CONVERSATION_GET_TURN, (params) => {
|
|
7537
|
+
const turnId = requiredString(params, "turnId");
|
|
7538
|
+
const view = supervisor.getTurn(turnId);
|
|
7539
|
+
if (!view) {
|
|
7540
|
+
throw new RpcError(RPC_ERROR.INVALID_PARAMS, `Unknown turnId: ${turnId}`);
|
|
7541
|
+
}
|
|
7542
|
+
return serializeTurnView(view, {
|
|
7543
|
+
// Optional — falls back to the serializer's DEFAULT_TURN_TEXT_LIMIT.
|
|
7544
|
+
maxTextLength: optionalInt(params, "maxTextLength", DEFAULT_TURN_TEXT_LIMIT, 1, 1e5)
|
|
7545
|
+
});
|
|
7546
|
+
});
|
|
6706
7547
|
rpc.register(METHOD.SHUTDOWN, () => {
|
|
6707
7548
|
setImmediate(() => {
|
|
6708
7549
|
void supervisor.shutdown().catch(() => {
|
|
@@ -6727,6 +7568,19 @@ function requiredString(params, key) {
|
|
|
6727
7568
|
}
|
|
6728
7569
|
return value;
|
|
6729
7570
|
}
|
|
7571
|
+
function optionalInt(params, key, defaultValue, min, max) {
|
|
7572
|
+
const raw = params[key];
|
|
7573
|
+
if (raw === void 0 || raw === null) {
|
|
7574
|
+
if (defaultValue === void 0) {
|
|
7575
|
+
throw new RpcError(RPC_ERROR.INVALID_PARAMS, `Missing required number param: ${key}`);
|
|
7576
|
+
}
|
|
7577
|
+
return defaultValue;
|
|
7578
|
+
}
|
|
7579
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < min || raw > max) {
|
|
7580
|
+
throw new RpcError(RPC_ERROR.INVALID_PARAMS, `Param ${key} must be an integer in [${min}, ${max}]`);
|
|
7581
|
+
}
|
|
7582
|
+
return raw;
|
|
7583
|
+
}
|
|
6730
7584
|
|
|
6731
7585
|
// src/server/main.ts
|
|
6732
7586
|
async function serverMain(options) {
|