pi-shadow-mind 0.1.13 → 0.1.15
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/DESIGN.md +24 -9
- package/README.md +15 -110
- package/README.zh-CN.md +110 -0
- package/dist/index.js +1033 -131
- package/dist/index.js.map +4 -4
- package/package.json +4 -5
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// src/runtime.ts
|
|
8
|
-
import { randomUUID } from "node:crypto";
|
|
8
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
9
9
|
import {
|
|
10
10
|
buildSessionContext
|
|
11
11
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -6760,6 +6760,12 @@ var browser_default = dist_exports;
|
|
|
6760
6760
|
// src/registry.ts
|
|
6761
6761
|
import { mkdir as mkdir2, readFile as readFile2, readdir, stat } from "node:fs/promises";
|
|
6762
6762
|
import { basename, extname, join as join2 } from "node:path";
|
|
6763
|
+
|
|
6764
|
+
// src/types.ts
|
|
6765
|
+
var DEFAULT_READ_TOOLS = ["read", "grep", "find", "ls"];
|
|
6766
|
+
var SHADOW_TRIGGERS = ["heartbeat", "final_response"];
|
|
6767
|
+
|
|
6768
|
+
// src/registry.ts
|
|
6763
6769
|
var ID_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
|
|
6764
6770
|
var ShadowRegistry = class {
|
|
6765
6771
|
directory;
|
|
@@ -6787,8 +6793,14 @@ var ShadowRegistry = class {
|
|
|
6787
6793
|
continue;
|
|
6788
6794
|
}
|
|
6789
6795
|
try {
|
|
6790
|
-
const definition = parseShadowMarkdown(
|
|
6791
|
-
|
|
6796
|
+
const definition = parseShadowMarkdown(
|
|
6797
|
+
await readFile2(filePath, "utf8"),
|
|
6798
|
+
filePath
|
|
6799
|
+
);
|
|
6800
|
+
this.cache.set(filePath, {
|
|
6801
|
+
mtimeMs: fileStat.mtimeMs,
|
|
6802
|
+
shadow: definition
|
|
6803
|
+
});
|
|
6792
6804
|
shadows.push(definition);
|
|
6793
6805
|
} catch (error) {
|
|
6794
6806
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -6803,7 +6815,10 @@ var ShadowRegistry = class {
|
|
|
6803
6815
|
const ids = /* @__PURE__ */ new Set();
|
|
6804
6816
|
for (const shadow of shadows) {
|
|
6805
6817
|
if (ids.has(shadow.id)) {
|
|
6806
|
-
diagnostics.push({
|
|
6818
|
+
diagnostics.push({
|
|
6819
|
+
filePath: shadow.filePath,
|
|
6820
|
+
message: `duplicate shadow id: ${shadow.id}`
|
|
6821
|
+
});
|
|
6807
6822
|
} else {
|
|
6808
6823
|
ids.add(shadow.id);
|
|
6809
6824
|
unique.push(shadow);
|
|
@@ -6813,14 +6828,18 @@ var ShadowRegistry = class {
|
|
|
6813
6828
|
}
|
|
6814
6829
|
};
|
|
6815
6830
|
function parseShadowMarkdown(source, filePath) {
|
|
6816
|
-
const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/.exec(
|
|
6831
|
+
const match = /^---\s*\r?\n([\s\S]*?)\r?\n---\s*(?:\r?\n|$)([\s\S]*)$/.exec(
|
|
6832
|
+
source
|
|
6833
|
+
);
|
|
6817
6834
|
if (!match) throw new Error("missing YAML frontmatter");
|
|
6818
6835
|
const meta = browser_default.parse(match[1]);
|
|
6819
|
-
if (!meta || typeof meta !== "object" || Array.isArray(meta))
|
|
6836
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta))
|
|
6837
|
+
throw new Error("frontmatter must be an object");
|
|
6820
6838
|
const value = meta;
|
|
6821
6839
|
const fallbackId = basename(filePath, extname(filePath));
|
|
6822
6840
|
const id = stringValue(value.id, fallbackId, "id");
|
|
6823
|
-
if (!ID_PATTERN.test(id))
|
|
6841
|
+
if (!ID_PATTERN.test(id))
|
|
6842
|
+
throw new Error("id must match [a-z0-9][a-z0-9_-]*");
|
|
6824
6843
|
const prompt = match[2].trim();
|
|
6825
6844
|
if (!prompt) throw new Error("shadow prompt body is empty");
|
|
6826
6845
|
return {
|
|
@@ -6828,11 +6847,23 @@ function parseShadowMarkdown(source, filePath) {
|
|
|
6828
6847
|
name: stringValue(value.name, id, "name"),
|
|
6829
6848
|
enabled: booleanValue(value.enabled, true, "enabled"),
|
|
6830
6849
|
debug: booleanValue(value.debug, false, "debug"),
|
|
6831
|
-
activationProbability: probabilityValue(
|
|
6832
|
-
|
|
6850
|
+
activationProbability: probabilityValue(
|
|
6851
|
+
value.activation_probability,
|
|
6852
|
+
0.3,
|
|
6853
|
+
"activation_probability"
|
|
6854
|
+
),
|
|
6855
|
+
trigger: triggerArray(value.trigger, ["heartbeat"], "trigger"),
|
|
6856
|
+
activeForModels: stringArray(
|
|
6857
|
+
value.active_for_models,
|
|
6858
|
+
["*"],
|
|
6859
|
+
"active_for_models"
|
|
6860
|
+
),
|
|
6833
6861
|
runWithModel: optionalString(value.run_with_model, "run_with_model"),
|
|
6834
6862
|
thinkingLevel: optionalThinking(value.thinking_level),
|
|
6835
|
-
timeoutSeconds: optionalPositiveNumber(
|
|
6863
|
+
timeoutSeconds: optionalPositiveNumber(
|
|
6864
|
+
value.timeout_seconds,
|
|
6865
|
+
"timeout_seconds"
|
|
6866
|
+
),
|
|
6836
6867
|
tools: stringArray(value.tools, [], "tools"),
|
|
6837
6868
|
prompt,
|
|
6838
6869
|
filePath
|
|
@@ -6840,7 +6871,8 @@ function parseShadowMarkdown(source, filePath) {
|
|
|
6840
6871
|
}
|
|
6841
6872
|
function stringValue(value, fallback, name) {
|
|
6842
6873
|
if (value === void 0) return fallback;
|
|
6843
|
-
if (!isNonEmptyString(value))
|
|
6874
|
+
if (!isNonEmptyString(value))
|
|
6875
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
6844
6876
|
return value.trim();
|
|
6845
6877
|
}
|
|
6846
6878
|
function optionalString(value, name) {
|
|
@@ -6853,19 +6885,31 @@ function booleanValue(value, fallback, name) {
|
|
|
6853
6885
|
}
|
|
6854
6886
|
function probabilityValue(value, fallback, name) {
|
|
6855
6887
|
if (value === void 0) return fallback;
|
|
6856
|
-
if (!isFiniteNumber(value) || !inRange(value, 0, 1))
|
|
6888
|
+
if (!isFiniteNumber(value) || !inRange(value, 0, 1))
|
|
6889
|
+
throw new Error(`${name} must be between 0 and 1`);
|
|
6857
6890
|
return value;
|
|
6858
6891
|
}
|
|
6859
6892
|
function optionalPositiveNumber(value, name) {
|
|
6860
6893
|
if (value === void 0) return void 0;
|
|
6861
|
-
if (!isFiniteNumber(value) || value <= 0)
|
|
6894
|
+
if (!isFiniteNumber(value) || value <= 0)
|
|
6895
|
+
throw new Error(`${name} must be positive`);
|
|
6862
6896
|
return value;
|
|
6863
6897
|
}
|
|
6864
6898
|
function stringArray(value, fallback, name) {
|
|
6865
6899
|
if (value === void 0) return [...fallback];
|
|
6866
|
-
if (!isStringArray(value))
|
|
6900
|
+
if (!isStringArray(value))
|
|
6901
|
+
throw new Error(`${name} must be an array of non-empty strings`);
|
|
6867
6902
|
return [...new Set(value.map((item) => item.trim()))];
|
|
6868
6903
|
}
|
|
6904
|
+
function triggerArray(value, fallback, name) {
|
|
6905
|
+
const values = typeof value === "string" ? [value] : value;
|
|
6906
|
+
const items = stringArray(values, fallback, name);
|
|
6907
|
+
const allowed = new Set(SHADOW_TRIGGERS);
|
|
6908
|
+
const invalid = items.filter((item) => !allowed.has(item));
|
|
6909
|
+
if (invalid.length)
|
|
6910
|
+
throw new Error(`${name} contains invalid trigger: ${invalid.join(", ")}`);
|
|
6911
|
+
return items;
|
|
6912
|
+
}
|
|
6869
6913
|
function optionalThinking(value) {
|
|
6870
6914
|
if (value === void 0) return void 0;
|
|
6871
6915
|
if (!isThinkingLevel(value)) throw new Error("thinking_level is invalid");
|
|
@@ -6887,17 +6931,37 @@ var EntityStore = class {
|
|
|
6887
6931
|
return shadow;
|
|
6888
6932
|
}
|
|
6889
6933
|
async create(draft) {
|
|
6890
|
-
if ((await this.list()).some((shadow) => shadow.id === draft.id))
|
|
6891
|
-
|
|
6934
|
+
if ((await this.list()).some((shadow) => shadow.id === draft.id))
|
|
6935
|
+
throw new Error(`shadow already exists: ${draft.id}`);
|
|
6936
|
+
return this.writeParsed(
|
|
6937
|
+
join3(this.registry.directory, `${draft.id}.md`),
|
|
6938
|
+
draft,
|
|
6939
|
+
{ overwrite: false }
|
|
6940
|
+
);
|
|
6892
6941
|
}
|
|
6893
6942
|
async update(id, patch) {
|
|
6894
6943
|
const current = await this.get(id);
|
|
6895
|
-
return this.writeParsed(
|
|
6944
|
+
return this.writeParsed(
|
|
6945
|
+
current.filePath,
|
|
6946
|
+
{
|
|
6947
|
+
...current,
|
|
6948
|
+
...definedOnly(patch),
|
|
6949
|
+
id,
|
|
6950
|
+
prompt: patch.prompt ?? current.prompt
|
|
6951
|
+
},
|
|
6952
|
+
{ overwrite: true }
|
|
6953
|
+
);
|
|
6896
6954
|
}
|
|
6897
6955
|
async writeParsed(filePath, draft, options) {
|
|
6898
|
-
const source = serializeShadow({
|
|
6956
|
+
const source = serializeShadow({
|
|
6957
|
+
...draft,
|
|
6958
|
+
prompt: draft.prompt ?? "Describe this Shadow Mind's responsibility."
|
|
6959
|
+
});
|
|
6899
6960
|
const parsed = parseShadowMarkdown(source, filePath);
|
|
6900
|
-
await writeFile2(filePath, source, {
|
|
6961
|
+
await writeFile2(filePath, source, {
|
|
6962
|
+
encoding: "utf8",
|
|
6963
|
+
...options.overwrite ? {} : { flag: "wx" }
|
|
6964
|
+
});
|
|
6901
6965
|
return parsed;
|
|
6902
6966
|
}
|
|
6903
6967
|
async setEnabled(id, enabled) {
|
|
@@ -6920,6 +6984,7 @@ function serializeShadow(shadow) {
|
|
|
6920
6984
|
enabled: shadow.enabled ?? true,
|
|
6921
6985
|
debug: shadow.debug ?? false,
|
|
6922
6986
|
activation_probability: shadow.activationProbability ?? 0.3,
|
|
6987
|
+
trigger: shadow.trigger ?? ["heartbeat"],
|
|
6923
6988
|
active_for_models: shadow.activeForModels ?? ["*"],
|
|
6924
6989
|
...shadow.runWithModel ? { run_with_model: shadow.runWithModel } : {},
|
|
6925
6990
|
...shadow.thinkingLevel ? { thinking_level: shadow.thinkingLevel } : {},
|
|
@@ -6934,21 +6999,68 @@ ${(shadow.prompt ?? "").trim()}
|
|
|
6934
6999
|
`;
|
|
6935
7000
|
}
|
|
6936
7001
|
function describeShadow(shadow) {
|
|
6937
|
-
return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} models=${shadow.activeForModels.join(",")} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
|
|
7002
|
+
return `${shadow.enabled ? "enabled" : "disabled"} ${shadow.id} (${shadow.name}) p=${shadow.activationProbability} trigger=${shadow.trigger.join(",")} models=${shadow.activeForModels.join(",")} tools=${shadow.tools.join(",") || "default"} file=${basename2(shadow.filePath)}`;
|
|
6938
7003
|
}
|
|
6939
7004
|
function definedOnly(value) {
|
|
6940
|
-
return Object.fromEntries(
|
|
7005
|
+
return Object.fromEntries(
|
|
7006
|
+
Object.entries(value).filter(([, item]) => item !== void 0)
|
|
7007
|
+
);
|
|
6941
7008
|
}
|
|
6942
7009
|
|
|
7010
|
+
// src/final-response-queue.ts
|
|
7011
|
+
var FinalResponseQueue = class {
|
|
7012
|
+
constructor(options) {
|
|
7013
|
+
this.options = options;
|
|
7014
|
+
}
|
|
7015
|
+
pending = [];
|
|
7016
|
+
enqueue(items) {
|
|
7017
|
+
this.pending.push(...items);
|
|
7018
|
+
this.pump();
|
|
7019
|
+
}
|
|
7020
|
+
slotAvailable() {
|
|
7021
|
+
this.pump();
|
|
7022
|
+
}
|
|
7023
|
+
clear() {
|
|
7024
|
+
this.pending.length = 0;
|
|
7025
|
+
}
|
|
7026
|
+
pump() {
|
|
7027
|
+
while (this.options.activeCount() < this.options.maxParallel() && this.pending.length > 0) {
|
|
7028
|
+
const epoch = this.options.currentEpoch();
|
|
7029
|
+
const activeShadowIds = this.options.activeShadowIds();
|
|
7030
|
+
const index = this.pending.findIndex(
|
|
7031
|
+
(item2) => item2.epoch !== epoch || !activeShadowIds.has(item2.shadowId)
|
|
7032
|
+
);
|
|
7033
|
+
if (index < 0) return;
|
|
7034
|
+
const [item] = this.pending.splice(index, 1);
|
|
7035
|
+
if (!item || item.epoch !== epoch) continue;
|
|
7036
|
+
this.options.launch(item);
|
|
7037
|
+
}
|
|
7038
|
+
}
|
|
7039
|
+
};
|
|
7040
|
+
|
|
6943
7041
|
// src/management-tools.ts
|
|
6944
7042
|
import { Type } from "typebox";
|
|
6945
7043
|
var ID = Type.String({ pattern: "^[a-z0-9][a-z0-9_-]*$" });
|
|
6946
|
-
var THINKING = Type.Union([
|
|
7044
|
+
var THINKING = Type.Union([
|
|
7045
|
+
Type.Literal("minimal"),
|
|
7046
|
+
Type.Literal("low"),
|
|
7047
|
+
Type.Literal("medium"),
|
|
7048
|
+
Type.Literal("high"),
|
|
7049
|
+
Type.Literal("xhigh"),
|
|
7050
|
+
Type.Literal("max")
|
|
7051
|
+
]);
|
|
7052
|
+
var TRIGGER = Type.Union([
|
|
7053
|
+
Type.Literal("heartbeat"),
|
|
7054
|
+
Type.Literal("final_response")
|
|
7055
|
+
]);
|
|
6947
7056
|
var SHADOW_FIELDS = {
|
|
6948
7057
|
name: Type.Optional(Type.String()),
|
|
6949
7058
|
enabled: Type.Optional(Type.Boolean()),
|
|
6950
7059
|
debug: Type.Optional(Type.Boolean()),
|
|
6951
|
-
activation_probability: Type.Optional(
|
|
7060
|
+
activation_probability: Type.Optional(
|
|
7061
|
+
Type.Number({ minimum: 0, maximum: 1 })
|
|
7062
|
+
),
|
|
7063
|
+
trigger: Type.Optional(Type.Array(TRIGGER)),
|
|
6952
7064
|
active_for_models: Type.Optional(Type.Array(Type.String())),
|
|
6953
7065
|
run_with_model: Type.Optional(Type.String()),
|
|
6954
7066
|
thinking_level: Type.Optional(THINKING),
|
|
@@ -6967,80 +7079,160 @@ function registerManagementTools(pi, store, getConfig) {
|
|
|
6967
7079
|
pi.registerTool(configWriteTool(store, getConfig));
|
|
6968
7080
|
}
|
|
6969
7081
|
function listTool(store) {
|
|
6970
|
-
return tool(
|
|
6971
|
-
|
|
6972
|
-
|
|
6973
|
-
|
|
7082
|
+
return tool(
|
|
7083
|
+
"list_shadows",
|
|
7084
|
+
"List Shadows",
|
|
7085
|
+
"List all valid globally configured Shadow Minds.",
|
|
7086
|
+
Type.Object({}),
|
|
7087
|
+
async () => {
|
|
7088
|
+
const shadows = await store.list();
|
|
7089
|
+
return textResult(
|
|
7090
|
+
shadows.length ? shadows.map(describeShadow).join("\n") : "No Shadow Minds configured."
|
|
7091
|
+
);
|
|
7092
|
+
}
|
|
7093
|
+
);
|
|
6974
7094
|
}
|
|
6975
7095
|
function createTool(store) {
|
|
6976
|
-
return tool(
|
|
6977
|
-
|
|
6978
|
-
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
7096
|
+
return tool(
|
|
7097
|
+
"create_shadow",
|
|
7098
|
+
"Create Shadow",
|
|
7099
|
+
"Create a global Shadow Mind definition after user confirmation.",
|
|
7100
|
+
Type.Object({ id: ID, ...SHADOW_FIELDS, prompt: Type.String() }),
|
|
7101
|
+
async (_id, params, _signal, _update, ctx) => {
|
|
7102
|
+
const raw = params;
|
|
7103
|
+
if (!await confirm(
|
|
7104
|
+
ctx,
|
|
7105
|
+
"Create Shadow Mind",
|
|
7106
|
+
`Create ${String(raw.id)} in the global registry?`
|
|
7107
|
+
))
|
|
7108
|
+
return textResult("Cancelled.");
|
|
7109
|
+
const shadow = await store.create(toDraft(raw));
|
|
7110
|
+
return textResult(`Created ${describeShadow(shadow)}`);
|
|
7111
|
+
}
|
|
7112
|
+
);
|
|
6982
7113
|
}
|
|
6983
7114
|
function updateTool(store) {
|
|
6984
|
-
return tool(
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
6988
|
-
|
|
6989
|
-
|
|
7115
|
+
return tool(
|
|
7116
|
+
"update_shadow",
|
|
7117
|
+
"Update Shadow",
|
|
7118
|
+
"Update a global Shadow Mind definition after user confirmation. The id is immutable.",
|
|
7119
|
+
Type.Object({ id: ID, ...SHADOW_FIELDS }),
|
|
7120
|
+
async (_id, params, _signal, _update, ctx) => {
|
|
7121
|
+
const raw = params;
|
|
7122
|
+
if (!await confirm(
|
|
7123
|
+
ctx,
|
|
7124
|
+
"Update Shadow Mind",
|
|
7125
|
+
`Apply changes to ${String(raw.id)}?`
|
|
7126
|
+
))
|
|
7127
|
+
return textResult("Cancelled.");
|
|
7128
|
+
const shadow = await store.update(String(raw.id), toPatch(raw));
|
|
7129
|
+
return textResult(`Updated ${describeShadow(shadow)}`);
|
|
7130
|
+
}
|
|
7131
|
+
);
|
|
6990
7132
|
}
|
|
6991
7133
|
function toggleTool(store, enabled) {
|
|
6992
7134
|
const action = enabled ? "enable" : "disable";
|
|
6993
|
-
return tool(
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
7135
|
+
return tool(
|
|
7136
|
+
`${action}_shadow`,
|
|
7137
|
+
`${enabled ? "Enable" : "Disable"} Shadow`,
|
|
7138
|
+
`${enabled ? "Enable" : "Disable"} a global Shadow Mind after user confirmation.`,
|
|
7139
|
+
Type.Object({ id: ID }),
|
|
7140
|
+
async (_id, params, _signal, _update, ctx) => {
|
|
7141
|
+
const id = String(params.id);
|
|
7142
|
+
if (!await confirm(
|
|
7143
|
+
ctx,
|
|
7144
|
+
`${enabled ? "Enable" : "Disable"} Shadow Mind`,
|
|
7145
|
+
`${action} ${id}?`
|
|
7146
|
+
))
|
|
7147
|
+
return textResult("Cancelled.");
|
|
7148
|
+
return textResult(describeShadow(await store.setEnabled(id, enabled)));
|
|
7149
|
+
}
|
|
7150
|
+
);
|
|
6998
7151
|
}
|
|
6999
7152
|
function deleteTool(store) {
|
|
7000
|
-
return tool(
|
|
7001
|
-
|
|
7002
|
-
|
|
7003
|
-
|
|
7004
|
-
|
|
7005
|
-
|
|
7153
|
+
return tool(
|
|
7154
|
+
"delete_shadow",
|
|
7155
|
+
"Delete Shadow",
|
|
7156
|
+
"Delete a Shadow Mind definition after user confirmation. Debug logs are retained.",
|
|
7157
|
+
Type.Object({ id: ID }),
|
|
7158
|
+
async (_id, params, _signal, _update, ctx) => {
|
|
7159
|
+
const id = String(params.id);
|
|
7160
|
+
if (!await confirm(
|
|
7161
|
+
ctx,
|
|
7162
|
+
"Delete Shadow Mind",
|
|
7163
|
+
`Delete ${id}? Its debug logs will be kept.`
|
|
7164
|
+
))
|
|
7165
|
+
return textResult("Cancelled.");
|
|
7166
|
+
await store.delete(id);
|
|
7167
|
+
return textResult(`Deleted ${id}; logs were retained.`);
|
|
7168
|
+
}
|
|
7169
|
+
);
|
|
7006
7170
|
}
|
|
7007
7171
|
function configReadTool(store) {
|
|
7008
|
-
return tool(
|
|
7172
|
+
return tool(
|
|
7173
|
+
"get_shadow_config",
|
|
7174
|
+
"Get Shadow Config",
|
|
7175
|
+
"Read the global Shadow Mind runtime configuration.",
|
|
7176
|
+
Type.Object({}),
|
|
7177
|
+
async () => textResult(await store.readConfig())
|
|
7178
|
+
);
|
|
7009
7179
|
}
|
|
7010
7180
|
function configWriteTool(store, getConfig) {
|
|
7011
|
-
return tool(
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
})
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7181
|
+
return tool(
|
|
7182
|
+
"update_shadow_config",
|
|
7183
|
+
"Update Shadow Config",
|
|
7184
|
+
"Update global Shadow Mind runtime configuration after user confirmation.",
|
|
7185
|
+
Type.Object({
|
|
7186
|
+
heartbeat_probability: Type.Optional(
|
|
7187
|
+
Type.Number({ minimum: 0, maximum: 1 })
|
|
7188
|
+
),
|
|
7189
|
+
max_parallel_shadows: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
7190
|
+
default_shadow_timeout_seconds: Type.Optional(
|
|
7191
|
+
Type.Number({ exclusiveMinimum: 0 })
|
|
7192
|
+
),
|
|
7193
|
+
headless_drain_timeout_seconds: Type.Optional(
|
|
7194
|
+
Type.Number({ exclusiveMinimum: 0 })
|
|
7195
|
+
),
|
|
7196
|
+
result_batch_window_ms: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
7197
|
+
default_shadow_model: Type.Optional(Type.String()),
|
|
7198
|
+
default_thinking_level: Type.Optional(THINKING),
|
|
7199
|
+
random_seed: Type.Optional(
|
|
7200
|
+
Type.Integer({ minimum: 0, maximum: 4294967295 })
|
|
7201
|
+
)
|
|
7202
|
+
}),
|
|
7203
|
+
async (_id, params, _signal, _update, ctx) => {
|
|
7204
|
+
const current = getConfig();
|
|
7205
|
+
const raw = params;
|
|
7206
|
+
const next = parseConfig({
|
|
7207
|
+
heartbeat_probability: raw.heartbeat_probability ?? current.heartbeatProbability,
|
|
7208
|
+
max_parallel_shadows: raw.max_parallel_shadows ?? current.maxParallelShadows,
|
|
7209
|
+
default_shadow_timeout_seconds: raw.default_shadow_timeout_seconds ?? current.defaultShadowTimeoutSeconds,
|
|
7210
|
+
headless_drain_timeout_seconds: raw.headless_drain_timeout_seconds ?? current.headlessDrainTimeoutSeconds,
|
|
7211
|
+
result_batch_window_ms: raw.result_batch_window_ms ?? current.resultBatchWindowMs,
|
|
7212
|
+
default_shadow_model: raw.default_shadow_model ?? current.defaultShadowModel,
|
|
7213
|
+
default_thinking_level: raw.default_thinking_level ?? current.defaultThinkingLevel,
|
|
7214
|
+
random_seed: raw.random_seed ?? current.randomSeed
|
|
7215
|
+
});
|
|
7216
|
+
if (!await confirm(
|
|
7217
|
+
ctx,
|
|
7218
|
+
"Update Shadow Config",
|
|
7219
|
+
`Apply this config?
|
|
7220
|
+
${JSON.stringify(next, null, 2)}`
|
|
7221
|
+
))
|
|
7222
|
+
return textResult("Cancelled.");
|
|
7223
|
+
await store.writeConfig(next);
|
|
7224
|
+
return textResult("Shadow Mind config updated.");
|
|
7225
|
+
}
|
|
7226
|
+
);
|
|
7038
7227
|
}
|
|
7039
7228
|
function tool(name, label, description, parameters, execute) {
|
|
7040
7229
|
return { name, label, description, parameters, execute };
|
|
7041
7230
|
}
|
|
7042
7231
|
async function confirm(ctx, title, message) {
|
|
7043
|
-
if (!ctx.hasUI)
|
|
7232
|
+
if (!ctx.hasUI)
|
|
7233
|
+
throw new Error(
|
|
7234
|
+
"This write requires a UI confirmation, but no dialog-capable UI is available."
|
|
7235
|
+
);
|
|
7044
7236
|
return ctx.ui.confirm(title, message);
|
|
7045
7237
|
}
|
|
7046
7238
|
function textResult(text) {
|
|
@@ -7055,6 +7247,7 @@ function toPatch(raw) {
|
|
|
7055
7247
|
enabled: raw.enabled,
|
|
7056
7248
|
debug: raw.debug,
|
|
7057
7249
|
activationProbability: raw.activation_probability,
|
|
7250
|
+
trigger: raw.trigger,
|
|
7058
7251
|
activeForModels: raw.active_for_models,
|
|
7059
7252
|
runWithModel: raw.run_with_model,
|
|
7060
7253
|
thinkingLevel: raw.thinking_level,
|
|
@@ -7125,16 +7318,34 @@ function createRandom(seed) {
|
|
|
7125
7318
|
function shouldEvaluateHeartbeat(toolResults) {
|
|
7126
7319
|
return toolResults.length > 0;
|
|
7127
7320
|
}
|
|
7321
|
+
function shouldEvaluateFinalResponse(messages) {
|
|
7322
|
+
const message = messages.at(-1);
|
|
7323
|
+
if (!message || typeof message !== "object" || message.role !== "assistant")
|
|
7324
|
+
return false;
|
|
7325
|
+
const content = message.content;
|
|
7326
|
+
if (typeof content === "string") return content.trim().length > 0;
|
|
7327
|
+
return Array.isArray(content) && content.some((block) => {
|
|
7328
|
+
if (!block || typeof block !== "object") return false;
|
|
7329
|
+
const value = block;
|
|
7330
|
+
return value.type === "text" && typeof value.text === "string" && value.text.trim().length > 0;
|
|
7331
|
+
});
|
|
7332
|
+
}
|
|
7128
7333
|
function decideHeartbeat(options) {
|
|
7129
7334
|
const random = options.random ?? Math.random;
|
|
7130
7335
|
const heartbeatRoll = random();
|
|
7131
7336
|
if (heartbeatRoll >= options.heartbeatProbability || options.availableSlots <= 0) {
|
|
7132
|
-
return {
|
|
7337
|
+
return {
|
|
7338
|
+
heartbeatRoll,
|
|
7339
|
+
activated: [],
|
|
7340
|
+
candidates: [],
|
|
7341
|
+
modelFiltered: [],
|
|
7342
|
+
runningExcluded: []
|
|
7343
|
+
};
|
|
7133
7344
|
}
|
|
7134
7345
|
const modelFiltered = [];
|
|
7135
7346
|
const runningExcluded = [];
|
|
7136
7347
|
const rolls = options.shadows.filter((shadow) => {
|
|
7137
|
-
if (!shadow.enabled) return false;
|
|
7348
|
+
if (!shadow.enabled || !hasTrigger(shadow, "heartbeat")) return false;
|
|
7138
7349
|
if (options.activeShadowIds.has(shadow.id)) {
|
|
7139
7350
|
runningExcluded.push(shadow.id);
|
|
7140
7351
|
return false;
|
|
@@ -7145,20 +7356,55 @@ function decideHeartbeat(options) {
|
|
|
7145
7356
|
}
|
|
7146
7357
|
return true;
|
|
7147
7358
|
}).map((shadow) => ({ shadow, roll: random() }));
|
|
7148
|
-
const hits = rolls.filter(
|
|
7149
|
-
|
|
7359
|
+
const hits = rolls.filter(
|
|
7360
|
+
({ shadow, roll }) => roll < shadow.activationProbability
|
|
7361
|
+
);
|
|
7362
|
+
const selected = sample(
|
|
7363
|
+
hits,
|
|
7364
|
+
Math.min(options.availableSlots, hits.length),
|
|
7365
|
+
random
|
|
7366
|
+
);
|
|
7150
7367
|
const selectedIds = new Set(selected.map(({ shadow }) => shadow.id));
|
|
7151
7368
|
return {
|
|
7152
7369
|
heartbeatRoll,
|
|
7153
7370
|
activated: selected,
|
|
7154
|
-
candidates: rolls.map(({ shadow, roll }) => ({
|
|
7371
|
+
candidates: rolls.map(({ shadow, roll }) => ({
|
|
7372
|
+
shadowId: shadow.id,
|
|
7373
|
+
roll,
|
|
7374
|
+
selected: selectedIds.has(shadow.id)
|
|
7375
|
+
})),
|
|
7155
7376
|
modelFiltered,
|
|
7156
7377
|
runningExcluded
|
|
7157
7378
|
};
|
|
7158
7379
|
}
|
|
7380
|
+
function decideFinalResponse(options) {
|
|
7381
|
+
const modelFiltered = [];
|
|
7382
|
+
const candidates = options.shadows.filter((shadow) => {
|
|
7383
|
+
if (!shadow.enabled || !hasTrigger(shadow, "final_response"))
|
|
7384
|
+
return false;
|
|
7385
|
+
if (!matchesModel(shadow, options.mainModelId)) {
|
|
7386
|
+
modelFiltered.push(shadow.id);
|
|
7387
|
+
return false;
|
|
7388
|
+
}
|
|
7389
|
+
return true;
|
|
7390
|
+
}).map((shadow) => ({ shadow, roll: 0 }));
|
|
7391
|
+
return {
|
|
7392
|
+
activated: candidates,
|
|
7393
|
+
candidates: candidates.map(({ shadow, roll }) => ({
|
|
7394
|
+
shadowId: shadow.id,
|
|
7395
|
+
roll,
|
|
7396
|
+
selected: true
|
|
7397
|
+
})),
|
|
7398
|
+
modelFiltered,
|
|
7399
|
+
runningExcluded: []
|
|
7400
|
+
};
|
|
7401
|
+
}
|
|
7159
7402
|
function matchesModel(shadow, fullModelId) {
|
|
7160
7403
|
return shadow.activeForModels.includes("*") || shadow.activeForModels.includes(fullModelId);
|
|
7161
7404
|
}
|
|
7405
|
+
function hasTrigger(shadow, trigger) {
|
|
7406
|
+
return shadow.trigger.includes(trigger);
|
|
7407
|
+
}
|
|
7162
7408
|
function sample(values, count, random) {
|
|
7163
7409
|
const copy = [...values];
|
|
7164
7410
|
for (let index = copy.length - 1; index > 0; index -= 1) {
|
|
@@ -7209,8 +7455,70 @@ import {
|
|
|
7209
7455
|
createAgentSession
|
|
7210
7456
|
} from "@earendil-works/pi-coding-agent";
|
|
7211
7457
|
|
|
7212
|
-
// src/
|
|
7213
|
-
|
|
7458
|
+
// src/usage.ts
|
|
7459
|
+
function zeroUsage() {
|
|
7460
|
+
return {
|
|
7461
|
+
requests: 0,
|
|
7462
|
+
input: 0,
|
|
7463
|
+
output: 0,
|
|
7464
|
+
cacheRead: 0,
|
|
7465
|
+
cacheWrite: 0,
|
|
7466
|
+
totalTokens: 0,
|
|
7467
|
+
cost: {
|
|
7468
|
+
input: 0,
|
|
7469
|
+
output: 0,
|
|
7470
|
+
cacheRead: 0,
|
|
7471
|
+
cacheWrite: 0,
|
|
7472
|
+
total: 0
|
|
7473
|
+
}
|
|
7474
|
+
};
|
|
7475
|
+
}
|
|
7476
|
+
function addUsage(left, right) {
|
|
7477
|
+
return {
|
|
7478
|
+
requests: left.requests + right.requests,
|
|
7479
|
+
input: left.input + right.input,
|
|
7480
|
+
output: left.output + right.output,
|
|
7481
|
+
cacheRead: left.cacheRead + right.cacheRead,
|
|
7482
|
+
cacheWrite: left.cacheWrite + right.cacheWrite,
|
|
7483
|
+
totalTokens: left.totalTokens + right.totalTokens,
|
|
7484
|
+
cost: {
|
|
7485
|
+
input: left.cost.input + right.cost.input,
|
|
7486
|
+
output: left.cost.output + right.cost.output,
|
|
7487
|
+
cacheRead: left.cost.cacheRead + right.cost.cacheRead,
|
|
7488
|
+
cacheWrite: left.cost.cacheWrite + right.cost.cacheWrite,
|
|
7489
|
+
total: left.cost.total + right.cost.total
|
|
7490
|
+
}
|
|
7491
|
+
};
|
|
7492
|
+
}
|
|
7493
|
+
function formatUsageTokens(tokens) {
|
|
7494
|
+
if (tokens < 1e3) return tokens.toLocaleString("en-US");
|
|
7495
|
+
if (tokens < 999950) return `${formatCompact(tokens / 1e3)}k`;
|
|
7496
|
+
return `${formatCompact(tokens / 1e6)}m`;
|
|
7497
|
+
}
|
|
7498
|
+
function formatUsageCost(cost) {
|
|
7499
|
+
if (cost === 0) return "$0";
|
|
7500
|
+
if (cost >= 0.01) return `$${cost.toFixed(2)}`;
|
|
7501
|
+
if (cost >= 1e-3) return `$${cost.toFixed(3)}`;
|
|
7502
|
+
return `$${cost.toFixed(4)}`;
|
|
7503
|
+
}
|
|
7504
|
+
function formatUsageSummary(usage) {
|
|
7505
|
+
return `${usage.requests} requests \xB7 ${formatUsageTokens(usage.totalTokens)} tokens \xB7 API ${formatUsageCost(usage.cost.total)}`;
|
|
7506
|
+
}
|
|
7507
|
+
function formatUsageDetail(scope, usage) {
|
|
7508
|
+
return [
|
|
7509
|
+
`usage ${scope}`,
|
|
7510
|
+
`${usage.requests} requests`,
|
|
7511
|
+
`${formatUsageTokens(usage.totalTokens)} total`,
|
|
7512
|
+
`${formatUsageTokens(usage.input)} input`,
|
|
7513
|
+
`${formatUsageTokens(usage.output)} output`,
|
|
7514
|
+
`${formatUsageTokens(usage.cacheRead)} cache read`,
|
|
7515
|
+
`${formatUsageTokens(usage.cacheWrite)} cache write`,
|
|
7516
|
+
`API ${formatUsageCost(usage.cost.total)}`
|
|
7517
|
+
].join(" \xB7 ");
|
|
7518
|
+
}
|
|
7519
|
+
function formatCompact(value) {
|
|
7520
|
+
return Number(value.toFixed(1)).toString();
|
|
7521
|
+
}
|
|
7214
7522
|
|
|
7215
7523
|
// src/protocol.ts
|
|
7216
7524
|
var RUNTIME_PROTOCOL = `You are a Shadow Mind running beside the main agent.
|
|
@@ -7374,6 +7682,7 @@ function buildRunResult(options) {
|
|
|
7374
7682
|
const { reason, error, durationMs, session, baseMessageCount, missingTools, thinkingLevel } = options;
|
|
7375
7683
|
const ownMessages = session ? session.messages.slice(baseMessageCount) : [];
|
|
7376
7684
|
const metrics = toolMetrics(ownMessages);
|
|
7685
|
+
const usage = usageMetrics(ownMessages);
|
|
7377
7686
|
return {
|
|
7378
7687
|
reason,
|
|
7379
7688
|
...error !== void 0 ? { error } : {},
|
|
@@ -7381,6 +7690,7 @@ function buildRunResult(options) {
|
|
|
7381
7690
|
toolNames: session?.getActiveToolNames() ?? [],
|
|
7382
7691
|
missingTools,
|
|
7383
7692
|
...metrics,
|
|
7693
|
+
usage,
|
|
7384
7694
|
...thinkingLevel !== void 0 ? { thinkingLevel } : {},
|
|
7385
7695
|
sessionFile: session?.sessionFile
|
|
7386
7696
|
};
|
|
@@ -7455,6 +7765,7 @@ var ShadowRunner = class {
|
|
|
7455
7765
|
extensions: base.extensions.filter((extension) => !isSelfExtension(extension.resolvedPath))
|
|
7456
7766
|
})
|
|
7457
7767
|
});
|
|
7768
|
+
await resourceLoader.reload();
|
|
7458
7769
|
const reportTool = createReportTool((content) => {
|
|
7459
7770
|
if (state.reported || controller.signal.aborted) return;
|
|
7460
7771
|
state.reported = true;
|
|
@@ -7501,11 +7812,33 @@ function toolMetrics(messages) {
|
|
|
7501
7812
|
}
|
|
7502
7813
|
const toolStats = [...stats.values()].sort((a, b) => b.calls - a.calls);
|
|
7503
7814
|
return {
|
|
7504
|
-
toolCalls: toolStats.reduce((sum,
|
|
7505
|
-
toolFailures: toolStats.reduce((sum,
|
|
7815
|
+
toolCalls: toolStats.reduce((sum, stat3) => sum + stat3.calls, 0),
|
|
7816
|
+
toolFailures: toolStats.reduce((sum, stat3) => sum + stat3.failures, 0),
|
|
7506
7817
|
toolStats
|
|
7507
7818
|
};
|
|
7508
7819
|
}
|
|
7820
|
+
function usageMetrics(messages) {
|
|
7821
|
+
let aggregate = zeroUsage();
|
|
7822
|
+
for (const message of messages) {
|
|
7823
|
+
if (message.role !== "assistant" || !message.usage) continue;
|
|
7824
|
+
aggregate = addUsage(aggregate, {
|
|
7825
|
+
requests: 1,
|
|
7826
|
+
input: message.usage.input,
|
|
7827
|
+
output: message.usage.output,
|
|
7828
|
+
cacheRead: message.usage.cacheRead,
|
|
7829
|
+
cacheWrite: message.usage.cacheWrite,
|
|
7830
|
+
totalTokens: message.usage.totalTokens,
|
|
7831
|
+
cost: {
|
|
7832
|
+
input: message.usage.cost.input,
|
|
7833
|
+
output: message.usage.cost.output,
|
|
7834
|
+
cacheRead: message.usage.cost.cacheRead,
|
|
7835
|
+
cacheWrite: message.usage.cost.cacheWrite,
|
|
7836
|
+
total: message.usage.cost.total
|
|
7837
|
+
}
|
|
7838
|
+
});
|
|
7839
|
+
}
|
|
7840
|
+
return aggregate;
|
|
7841
|
+
}
|
|
7509
7842
|
function createReportTool(onReport) {
|
|
7510
7843
|
return {
|
|
7511
7844
|
name: "report_to_main",
|
|
@@ -7566,7 +7899,7 @@ async function waitForSettled(options) {
|
|
|
7566
7899
|
const pollMs = options.pollMs ?? 25;
|
|
7567
7900
|
const quietMs = options.quietMs ?? 50;
|
|
7568
7901
|
const now = options.now ?? Date.now;
|
|
7569
|
-
const
|
|
7902
|
+
const delay2 = options.delay ?? ((ms) => new Promise((resolve2) => setTimeout(resolve2, ms)));
|
|
7570
7903
|
const started = now();
|
|
7571
7904
|
let settledSince;
|
|
7572
7905
|
while (now() - started < options.timeoutMs) {
|
|
@@ -7577,24 +7910,407 @@ async function waitForSettled(options) {
|
|
|
7577
7910
|
settledSince = void 0;
|
|
7578
7911
|
}
|
|
7579
7912
|
const remaining = options.timeoutMs - (now() - started);
|
|
7580
|
-
await
|
|
7913
|
+
await delay2(Math.min(pollMs, Math.max(1, remaining)));
|
|
7581
7914
|
}
|
|
7582
7915
|
return { settled: false, durationMs: now() - started };
|
|
7583
7916
|
}
|
|
7584
7917
|
|
|
7918
|
+
// src/usage-store.ts
|
|
7919
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
7920
|
+
import { mkdir as mkdir5, readFile as readFile5, rename as rename2, rm as rm2, writeFile as writeFile4 } from "node:fs/promises";
|
|
7921
|
+
import { dirname as dirname3, join as join6 } from "node:path";
|
|
7922
|
+
|
|
7923
|
+
// src/usage-file-lock.ts
|
|
7924
|
+
import { randomUUID } from "node:crypto";
|
|
7925
|
+
import { mkdir as mkdir4, readFile as readFile4, rename, rm, stat as stat2, utimes, writeFile as writeFile3 } from "node:fs/promises";
|
|
7926
|
+
import { join as join5 } from "node:path";
|
|
7927
|
+
var USAGE_LOCK_OWNER_FILE = "owner";
|
|
7928
|
+
var USAGE_LOCK_OPERATION_DIRECTORY = ".operation";
|
|
7929
|
+
async function acquireUsageFileLock(usagePath, timing = {}) {
|
|
7930
|
+
const resolved = resolveTiming(timing);
|
|
7931
|
+
const lockPath = `${usagePath}.lock`;
|
|
7932
|
+
const owner = { token: randomUUID(), pid: process.pid };
|
|
7933
|
+
while (true) {
|
|
7934
|
+
try {
|
|
7935
|
+
await mkdir4(lockPath);
|
|
7936
|
+
} catch (error) {
|
|
7937
|
+
if (error.code !== "EEXIST") throw error;
|
|
7938
|
+
await recoverStaleUsageLock(lockPath, resolved);
|
|
7939
|
+
await delay(resolved.retryMs);
|
|
7940
|
+
continue;
|
|
7941
|
+
}
|
|
7942
|
+
try {
|
|
7943
|
+
await writeOwner(lockPath, owner);
|
|
7944
|
+
} catch (error) {
|
|
7945
|
+
await rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
7946
|
+
throw error;
|
|
7947
|
+
}
|
|
7948
|
+
return startLease(lockPath, owner, resolved);
|
|
7949
|
+
}
|
|
7950
|
+
}
|
|
7951
|
+
function startLease(lockPath, owner, timing) {
|
|
7952
|
+
let stopped = false;
|
|
7953
|
+
let heartbeat = Promise.resolve();
|
|
7954
|
+
const timer = setInterval(() => {
|
|
7955
|
+
heartbeat = heartbeat.then(async () => {
|
|
7956
|
+
if (stopped) return;
|
|
7957
|
+
const current = await readUsageLockState(lockPath);
|
|
7958
|
+
if (!current || current.owner?.token !== owner.token) {
|
|
7959
|
+
stopped = true;
|
|
7960
|
+
return;
|
|
7961
|
+
}
|
|
7962
|
+
const now = /* @__PURE__ */ new Date();
|
|
7963
|
+
await utimes(join5(lockPath, USAGE_LOCK_OWNER_FILE), now, now);
|
|
7964
|
+
}).catch(() => void 0);
|
|
7965
|
+
}, timing.heartbeatMs);
|
|
7966
|
+
timer.unref();
|
|
7967
|
+
return async () => {
|
|
7968
|
+
stopped = true;
|
|
7969
|
+
clearInterval(timer);
|
|
7970
|
+
await heartbeat;
|
|
7971
|
+
await releaseUsageLock(lockPath, owner.token, timing);
|
|
7972
|
+
};
|
|
7973
|
+
}
|
|
7974
|
+
async function recoverStaleUsageLock(lockPath, timing) {
|
|
7975
|
+
const observed = await readUsageLockState(lockPath);
|
|
7976
|
+
if (!observed || !isRecoverableUsageLock(observed, timing.staleMs)) return;
|
|
7977
|
+
const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
|
|
7978
|
+
if (!await claimUsageLockOperation(operationPath, timing)) return;
|
|
7979
|
+
try {
|
|
7980
|
+
const current = await readUsageLockState(lockPath);
|
|
7981
|
+
if (!current || !sameUsageLock(current, observed) || !isRecoverableUsageLock(current, timing.staleMs)) return;
|
|
7982
|
+
const abandonedPath = `${lockPath}.${randomUUID()}.stale`;
|
|
7983
|
+
try {
|
|
7984
|
+
await rename(lockPath, abandonedPath);
|
|
7985
|
+
} catch (error) {
|
|
7986
|
+
if (error.code === "ENOENT") return;
|
|
7987
|
+
throw error;
|
|
7988
|
+
}
|
|
7989
|
+
await rm(abandonedPath, { recursive: true, force: true });
|
|
7990
|
+
} finally {
|
|
7991
|
+
await rm(operationPath, { recursive: true, force: true }).catch(() => void 0);
|
|
7992
|
+
}
|
|
7993
|
+
}
|
|
7994
|
+
async function releaseUsageLock(lockPath, token, timing) {
|
|
7995
|
+
const operationPath = join5(lockPath, USAGE_LOCK_OPERATION_DIRECTORY);
|
|
7996
|
+
while (true) {
|
|
7997
|
+
const observed = await readUsageLockState(lockPath);
|
|
7998
|
+
if (!observed || observed.owner?.token !== token) return;
|
|
7999
|
+
if (!await claimUsageLockOperation(operationPath, timing)) {
|
|
8000
|
+
await delay(timing.retryMs);
|
|
8001
|
+
continue;
|
|
8002
|
+
}
|
|
8003
|
+
try {
|
|
8004
|
+
const current = await readUsageLockState(lockPath);
|
|
8005
|
+
if (!current || current.owner?.token !== token) return;
|
|
8006
|
+
const releasedPath = `${lockPath}.${token}.released`;
|
|
8007
|
+
try {
|
|
8008
|
+
await rename(lockPath, releasedPath);
|
|
8009
|
+
} catch (error) {
|
|
8010
|
+
if (error.code === "ENOENT") continue;
|
|
8011
|
+
throw error;
|
|
8012
|
+
}
|
|
8013
|
+
await rm(releasedPath, { recursive: true, force: true });
|
|
8014
|
+
return;
|
|
8015
|
+
} finally {
|
|
8016
|
+
await rm(operationPath, { recursive: true, force: true }).catch(() => void 0);
|
|
8017
|
+
}
|
|
8018
|
+
}
|
|
8019
|
+
}
|
|
8020
|
+
async function claimUsageLockOperation(operationPath, timing) {
|
|
8021
|
+
const owner = { token: randomUUID(), pid: process.pid };
|
|
8022
|
+
try {
|
|
8023
|
+
await mkdir4(operationPath);
|
|
8024
|
+
try {
|
|
8025
|
+
await writeOwner(operationPath, owner);
|
|
8026
|
+
} catch (error) {
|
|
8027
|
+
await rm(operationPath, { recursive: true, force: true }).catch(() => void 0);
|
|
8028
|
+
throw error;
|
|
8029
|
+
}
|
|
8030
|
+
return true;
|
|
8031
|
+
} catch (error) {
|
|
8032
|
+
if (error.code !== "EEXIST") {
|
|
8033
|
+
if (error.code === "ENOENT") return false;
|
|
8034
|
+
throw error;
|
|
8035
|
+
}
|
|
8036
|
+
}
|
|
8037
|
+
const observed = await readUsageLockState(operationPath);
|
|
8038
|
+
if (!observed || !isRecoverableUsageLock(observed, timing.staleMs)) return false;
|
|
8039
|
+
const abandonedPath = `${operationPath}.${randomUUID()}.stale`;
|
|
8040
|
+
try {
|
|
8041
|
+
await rename(operationPath, abandonedPath);
|
|
8042
|
+
} catch (error) {
|
|
8043
|
+
if (error.code === "ENOENT") return false;
|
|
8044
|
+
throw error;
|
|
8045
|
+
}
|
|
8046
|
+
await rm(abandonedPath, { recursive: true, force: true });
|
|
8047
|
+
return false;
|
|
8048
|
+
}
|
|
8049
|
+
async function writeOwner(path, owner) {
|
|
8050
|
+
await writeFile3(join5(path, USAGE_LOCK_OWNER_FILE), JSON.stringify(owner), {
|
|
8051
|
+
encoding: "utf8",
|
|
8052
|
+
flag: "wx"
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
async function readUsageLockState(lockPath) {
|
|
8056
|
+
let lock;
|
|
8057
|
+
try {
|
|
8058
|
+
lock = await stat2(lockPath);
|
|
8059
|
+
} catch (error) {
|
|
8060
|
+
if (error.code === "ENOENT") return void 0;
|
|
8061
|
+
throw error;
|
|
8062
|
+
}
|
|
8063
|
+
try {
|
|
8064
|
+
const ownerPath = join5(lockPath, USAGE_LOCK_OWNER_FILE);
|
|
8065
|
+
const [raw, ownerFile] = await Promise.all([readFile4(ownerPath, "utf8"), stat2(ownerPath)]);
|
|
8066
|
+
return {
|
|
8067
|
+
owner: parseOwner(raw),
|
|
8068
|
+
mtimeMs: ownerFile.mtimeMs,
|
|
8069
|
+
dev: lock.dev,
|
|
8070
|
+
ino: lock.ino
|
|
8071
|
+
};
|
|
8072
|
+
} catch (error) {
|
|
8073
|
+
if (error.code !== "ENOENT") throw error;
|
|
8074
|
+
return { mtimeMs: lock.mtimeMs, dev: lock.dev, ino: lock.ino };
|
|
8075
|
+
}
|
|
8076
|
+
}
|
|
8077
|
+
function parseOwner(raw) {
|
|
8078
|
+
try {
|
|
8079
|
+
const value = JSON.parse(raw);
|
|
8080
|
+
if (typeof value.token !== "string" || !value.token || !Number.isSafeInteger(value.pid) || value.pid <= 0) return void 0;
|
|
8081
|
+
return { token: value.token, pid: value.pid };
|
|
8082
|
+
} catch {
|
|
8083
|
+
return void 0;
|
|
8084
|
+
}
|
|
8085
|
+
}
|
|
8086
|
+
function isRecoverableUsageLock(lock, staleMs) {
|
|
8087
|
+
return Date.now() - lock.mtimeMs >= staleMs && !isProcessAlive(lock.owner?.pid);
|
|
8088
|
+
}
|
|
8089
|
+
function isProcessAlive(pid) {
|
|
8090
|
+
if (pid === void 0) return false;
|
|
8091
|
+
try {
|
|
8092
|
+
process.kill(pid, 0);
|
|
8093
|
+
return true;
|
|
8094
|
+
} catch (error) {
|
|
8095
|
+
return error.code !== "ESRCH";
|
|
8096
|
+
}
|
|
8097
|
+
}
|
|
8098
|
+
function sameUsageLock(left, right) {
|
|
8099
|
+
return left.owner !== void 0 && right.owner !== void 0 ? left.owner.token === right.owner.token : left.owner === right.owner && left.dev === right.dev && left.ino === right.ino;
|
|
8100
|
+
}
|
|
8101
|
+
function resolveTiming(timing) {
|
|
8102
|
+
return {
|
|
8103
|
+
retryMs: timing.retryMs ?? 25,
|
|
8104
|
+
staleMs: timing.staleMs ?? 3e4,
|
|
8105
|
+
heartbeatMs: timing.heartbeatMs ?? 1e4
|
|
8106
|
+
};
|
|
8107
|
+
}
|
|
8108
|
+
function delay(ms) {
|
|
8109
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
8110
|
+
}
|
|
8111
|
+
|
|
8112
|
+
// src/usage-store.ts
|
|
8113
|
+
var USAGE_DOCUMENT_VERSION = 1;
|
|
8114
|
+
var UsageStore = class {
|
|
8115
|
+
usagePath;
|
|
8116
|
+
lifetime = zeroUsage();
|
|
8117
|
+
pendingUsage = zeroUsage();
|
|
8118
|
+
lastError;
|
|
8119
|
+
initialization;
|
|
8120
|
+
pendingWrite = Promise.resolve();
|
|
8121
|
+
constructor(agentDir) {
|
|
8122
|
+
this.usagePath = join6(agentDir, "shadow-minds", "usage.json");
|
|
8123
|
+
}
|
|
8124
|
+
initialize() {
|
|
8125
|
+
this.initialization ??= this.load();
|
|
8126
|
+
return this.initialization;
|
|
8127
|
+
}
|
|
8128
|
+
get current() {
|
|
8129
|
+
return {
|
|
8130
|
+
...this.lifetime,
|
|
8131
|
+
cost: { ...this.lifetime.cost }
|
|
8132
|
+
};
|
|
8133
|
+
}
|
|
8134
|
+
get error() {
|
|
8135
|
+
return this.lastError;
|
|
8136
|
+
}
|
|
8137
|
+
add(usage) {
|
|
8138
|
+
this.lifetime = addUsage(this.lifetime, usage);
|
|
8139
|
+
this.pendingUsage = addUsage(this.pendingUsage, usage);
|
|
8140
|
+
return this.enqueueWrite();
|
|
8141
|
+
}
|
|
8142
|
+
async flush() {
|
|
8143
|
+
await this.pendingWrite;
|
|
8144
|
+
if (!isZeroUsage(this.pendingUsage)) await this.enqueueWrite();
|
|
8145
|
+
}
|
|
8146
|
+
enqueueWrite() {
|
|
8147
|
+
this.pendingWrite = this.pendingWrite.then(
|
|
8148
|
+
() => this.writePending(),
|
|
8149
|
+
() => this.writePending()
|
|
8150
|
+
).then(
|
|
8151
|
+
() => {
|
|
8152
|
+
this.lastError = void 0;
|
|
8153
|
+
},
|
|
8154
|
+
(error) => {
|
|
8155
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
8156
|
+
}
|
|
8157
|
+
);
|
|
8158
|
+
return this.pendingWrite;
|
|
8159
|
+
}
|
|
8160
|
+
async load() {
|
|
8161
|
+
try {
|
|
8162
|
+
const raw = await readFile5(this.usagePath, "utf8");
|
|
8163
|
+
this.lifetime = parseUsageDocument(JSON.parse(raw)).lifetime;
|
|
8164
|
+
this.lastError = void 0;
|
|
8165
|
+
} catch (error) {
|
|
8166
|
+
if (error.code === "ENOENT") {
|
|
8167
|
+
this.lifetime = zeroUsage();
|
|
8168
|
+
this.lastError = void 0;
|
|
8169
|
+
return;
|
|
8170
|
+
}
|
|
8171
|
+
this.lifetime = zeroUsage();
|
|
8172
|
+
this.lastError = error instanceof Error ? error.message : String(error);
|
|
8173
|
+
}
|
|
8174
|
+
}
|
|
8175
|
+
async writePending() {
|
|
8176
|
+
const delta = this.pendingUsage;
|
|
8177
|
+
if (isZeroUsage(delta)) return;
|
|
8178
|
+
await mkdir5(dirname3(this.usagePath), { recursive: true });
|
|
8179
|
+
const releaseLock = await acquireUsageFileLock(this.usagePath);
|
|
8180
|
+
try {
|
|
8181
|
+
const committed = addUsage(await readUsageLifetime(this.usagePath), delta);
|
|
8182
|
+
await writeUsageDocument(this.usagePath, committed);
|
|
8183
|
+
this.pendingUsage = subtractUsage(this.pendingUsage, delta);
|
|
8184
|
+
this.lifetime = addUsage(committed, this.pendingUsage);
|
|
8185
|
+
} finally {
|
|
8186
|
+
await releaseLock();
|
|
8187
|
+
}
|
|
8188
|
+
}
|
|
8189
|
+
};
|
|
8190
|
+
function subtractUsage(left, right) {
|
|
8191
|
+
return {
|
|
8192
|
+
requests: left.requests - right.requests,
|
|
8193
|
+
input: left.input - right.input,
|
|
8194
|
+
output: left.output - right.output,
|
|
8195
|
+
cacheRead: left.cacheRead - right.cacheRead,
|
|
8196
|
+
cacheWrite: left.cacheWrite - right.cacheWrite,
|
|
8197
|
+
totalTokens: left.totalTokens - right.totalTokens,
|
|
8198
|
+
cost: {
|
|
8199
|
+
input: left.cost.input - right.cost.input,
|
|
8200
|
+
output: left.cost.output - right.cost.output,
|
|
8201
|
+
cacheRead: left.cost.cacheRead - right.cost.cacheRead,
|
|
8202
|
+
cacheWrite: left.cost.cacheWrite - right.cost.cacheWrite,
|
|
8203
|
+
total: left.cost.total - right.cost.total
|
|
8204
|
+
}
|
|
8205
|
+
};
|
|
8206
|
+
}
|
|
8207
|
+
function isZeroUsage(usage) {
|
|
8208
|
+
return usage.requests === 0 && usage.input === 0 && usage.output === 0 && usage.cacheRead === 0 && usage.cacheWrite === 0 && usage.totalTokens === 0 && usage.cost.input === 0 && usage.cost.output === 0 && usage.cost.cacheRead === 0 && usage.cost.cacheWrite === 0 && usage.cost.total === 0;
|
|
8209
|
+
}
|
|
8210
|
+
async function readUsageLifetime(usagePath) {
|
|
8211
|
+
try {
|
|
8212
|
+
const raw = await readFile5(usagePath, "utf8");
|
|
8213
|
+
return parseUsageDocument(JSON.parse(raw)).lifetime;
|
|
8214
|
+
} catch (error) {
|
|
8215
|
+
if (error.code === "ENOENT") return zeroUsage();
|
|
8216
|
+
throw error;
|
|
8217
|
+
}
|
|
8218
|
+
}
|
|
8219
|
+
async function writeUsageDocument(usagePath, lifetime) {
|
|
8220
|
+
const temporaryPath = `${usagePath}.${randomUUID2()}.tmp`;
|
|
8221
|
+
try {
|
|
8222
|
+
const document = { version: USAGE_DOCUMENT_VERSION, lifetime };
|
|
8223
|
+
await writeFile4(temporaryPath, `${JSON.stringify(document, null, 2)}
|
|
8224
|
+
`, "utf8");
|
|
8225
|
+
await rename2(temporaryPath, usagePath);
|
|
8226
|
+
} finally {
|
|
8227
|
+
await rm2(temporaryPath, { force: true }).catch(() => void 0);
|
|
8228
|
+
}
|
|
8229
|
+
}
|
|
8230
|
+
function parseUsageDocument(input) {
|
|
8231
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
8232
|
+
throw new Error("usage must be a JSON object");
|
|
8233
|
+
}
|
|
8234
|
+
const value = input;
|
|
8235
|
+
if (value.version !== USAGE_DOCUMENT_VERSION) throw new Error("usage version is unsupported");
|
|
8236
|
+
return { version: USAGE_DOCUMENT_VERSION, lifetime: parseUsage(value.lifetime, "lifetime") };
|
|
8237
|
+
}
|
|
8238
|
+
function parseUsage(input, name) {
|
|
8239
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
8240
|
+
throw new Error(`${name} must be an object`);
|
|
8241
|
+
}
|
|
8242
|
+
const value = input;
|
|
8243
|
+
const cost = value.cost;
|
|
8244
|
+
if (cost === null || typeof cost !== "object" || Array.isArray(cost)) {
|
|
8245
|
+
throw new Error(`${name}.cost must be an object`);
|
|
8246
|
+
}
|
|
8247
|
+
const costValue = cost;
|
|
8248
|
+
return {
|
|
8249
|
+
requests: nonNegativeInteger2(value.requests, `${name}.requests`),
|
|
8250
|
+
input: nonNegativeNumber(value.input, `${name}.input`),
|
|
8251
|
+
output: nonNegativeNumber(value.output, `${name}.output`),
|
|
8252
|
+
cacheRead: nonNegativeNumber(value.cacheRead, `${name}.cacheRead`),
|
|
8253
|
+
cacheWrite: nonNegativeNumber(value.cacheWrite, `${name}.cacheWrite`),
|
|
8254
|
+
totalTokens: nonNegativeNumber(value.totalTokens, `${name}.totalTokens`),
|
|
8255
|
+
cost: {
|
|
8256
|
+
input: nonNegativeNumber(costValue.input, `${name}.cost.input`),
|
|
8257
|
+
output: nonNegativeNumber(costValue.output, `${name}.cost.output`),
|
|
8258
|
+
cacheRead: nonNegativeNumber(costValue.cacheRead, `${name}.cost.cacheRead`),
|
|
8259
|
+
cacheWrite: nonNegativeNumber(costValue.cacheWrite, `${name}.cost.cacheWrite`),
|
|
8260
|
+
total: nonNegativeNumber(costValue.total, `${name}.cost.total`)
|
|
8261
|
+
}
|
|
8262
|
+
};
|
|
8263
|
+
}
|
|
8264
|
+
function nonNegativeNumber(value, name) {
|
|
8265
|
+
if (!isFiniteNumber(value) || value < 0) {
|
|
8266
|
+
throw new Error(`${name} must be a non-negative finite number`);
|
|
8267
|
+
}
|
|
8268
|
+
return value;
|
|
8269
|
+
}
|
|
8270
|
+
function nonNegativeInteger2(value, name) {
|
|
8271
|
+
const number = nonNegativeNumber(value, name);
|
|
8272
|
+
if (!Number.isSafeInteger(number)) throw new Error(`${name} must be a non-negative safe integer`);
|
|
8273
|
+
return number;
|
|
8274
|
+
}
|
|
8275
|
+
|
|
7585
8276
|
// src/runtime.ts
|
|
8277
|
+
var SESSION_TEARDOWN_TIMEOUT_MS = 1e3;
|
|
7586
8278
|
var ShadowMindRuntime = class {
|
|
7587
8279
|
constructor(pi) {
|
|
7588
8280
|
this.pi = pi;
|
|
7589
|
-
this.batcher = new ReportBatcher(
|
|
8281
|
+
this.batcher = new ReportBatcher(
|
|
8282
|
+
this.configStore.current.resultBatchWindowMs,
|
|
8283
|
+
(reports) => this.deliverReports(reports)
|
|
8284
|
+
);
|
|
7590
8285
|
}
|
|
7591
8286
|
agentDir = getAgentDir();
|
|
7592
8287
|
configStore = new ConfigStore(this.agentDir);
|
|
7593
8288
|
registry = new ShadowRegistry(this.agentDir);
|
|
7594
|
-
entityStore = new EntityStore(
|
|
8289
|
+
entityStore = new EntityStore(
|
|
8290
|
+
this.registry,
|
|
8291
|
+
this.configStore.configPath
|
|
8292
|
+
);
|
|
7595
8293
|
runner = new ShadowRunner();
|
|
8294
|
+
usageStore = new UsageStore(this.agentDir);
|
|
7596
8295
|
active = /* @__PURE__ */ new Map();
|
|
8296
|
+
finalResponseQueue = new FinalResponseQueue(
|
|
8297
|
+
{
|
|
8298
|
+
currentEpoch: () => this.epoch,
|
|
8299
|
+
maxParallel: () => this.configStore.current.maxParallelShadows,
|
|
8300
|
+
activeCount: () => this.active.size,
|
|
8301
|
+
activeShadowIds: () => new Set([...this.active.values()].map(({ shadow }) => shadow.id)),
|
|
8302
|
+
launch: (pending) => this.launchShadow(
|
|
8303
|
+
pending.ctx,
|
|
8304
|
+
pending.shadow,
|
|
8305
|
+
pending.mainModel,
|
|
8306
|
+
pending.fullModelId,
|
|
8307
|
+
pending.context,
|
|
8308
|
+
pending.availableTools
|
|
8309
|
+
)
|
|
8310
|
+
}
|
|
8311
|
+
);
|
|
7597
8312
|
recentEvents = [];
|
|
8313
|
+
recentRuns = [];
|
|
7598
8314
|
batcher;
|
|
7599
8315
|
sessionLifetime = new SessionLifetime();
|
|
7600
8316
|
epoch = 0;
|
|
@@ -7603,10 +8319,16 @@ var ShadowMindRuntime = class {
|
|
|
7603
8319
|
panelVisible = false;
|
|
7604
8320
|
latestContext;
|
|
7605
8321
|
diagnostics = [];
|
|
8322
|
+
sessionUsage = zeroUsage();
|
|
7606
8323
|
shadowCount = 0;
|
|
8324
|
+
completedWithFinalText = false;
|
|
7607
8325
|
random = Math.random;
|
|
7608
8326
|
register() {
|
|
7609
|
-
registerManagementTools(
|
|
8327
|
+
registerManagementTools(
|
|
8328
|
+
this.pi,
|
|
8329
|
+
this.entityStore,
|
|
8330
|
+
() => this.configStore.current
|
|
8331
|
+
);
|
|
7610
8332
|
this.registerEvents();
|
|
7611
8333
|
this.registerUi();
|
|
7612
8334
|
}
|
|
@@ -7615,16 +8337,23 @@ var ShadowMindRuntime = class {
|
|
|
7615
8337
|
this.sessionLifetime.activate();
|
|
7616
8338
|
this.latestContext = ctx;
|
|
7617
8339
|
this.modelCalls = 0;
|
|
8340
|
+
this.completedWithFinalText = false;
|
|
8341
|
+
this.sessionUsage = zeroUsage();
|
|
8342
|
+
this.recentRuns.length = 0;
|
|
7618
8343
|
await this.configStore.initialize();
|
|
8344
|
+
await this.usageStore.initialize();
|
|
7619
8345
|
this.random = createRandom(this.configStore.current.randomSeed);
|
|
7620
8346
|
await this.registry.initialize();
|
|
7621
8347
|
await this.refresh(ctx);
|
|
7622
|
-
this.record("session-config", {
|
|
8348
|
+
this.record("session-config", {
|
|
8349
|
+
randomSeed: this.configStore.current.randomSeed ?? "random"
|
|
8350
|
+
});
|
|
7623
8351
|
this.updateStatus(ctx);
|
|
7624
8352
|
});
|
|
7625
8353
|
this.pi.on("input", (event, ctx) => {
|
|
7626
8354
|
this.latestContext = ctx;
|
|
7627
8355
|
if (event.source === "extension") return;
|
|
8356
|
+
this.completedWithFinalText = false;
|
|
7628
8357
|
this.epoch += 1;
|
|
7629
8358
|
this.abortAll("new-user-input");
|
|
7630
8359
|
});
|
|
@@ -7635,11 +8364,29 @@ var ShadowMindRuntime = class {
|
|
|
7635
8364
|
this.pi.on("turn_end", async (event, ctx) => {
|
|
7636
8365
|
this.latestContext = ctx;
|
|
7637
8366
|
if (!shouldEvaluateHeartbeat(event.toolResults)) {
|
|
7638
|
-
this.record("heartbeat-skipped", {
|
|
8367
|
+
this.record("heartbeat-skipped", {
|
|
8368
|
+
reason: "no-tool-activity",
|
|
8369
|
+
modelCalls: this.modelCalls
|
|
8370
|
+
});
|
|
7639
8371
|
return;
|
|
7640
8372
|
}
|
|
7641
8373
|
await this.onHeartbeat(ctx);
|
|
7642
8374
|
});
|
|
8375
|
+
this.pi.on("agent_end", (event, ctx) => {
|
|
8376
|
+
this.latestContext = ctx;
|
|
8377
|
+
this.completedWithFinalText = shouldEvaluateFinalResponse(event.messages);
|
|
8378
|
+
});
|
|
8379
|
+
this.pi.on("agent_settled", async (_event, ctx) => {
|
|
8380
|
+
this.latestContext = ctx;
|
|
8381
|
+
if (!this.completedWithFinalText) {
|
|
8382
|
+
this.record("final-response-skipped", {
|
|
8383
|
+
reason: "no-final-assistant-text"
|
|
8384
|
+
});
|
|
8385
|
+
return;
|
|
8386
|
+
}
|
|
8387
|
+
this.completedWithFinalText = false;
|
|
8388
|
+
await this.onFinalResponse(ctx);
|
|
8389
|
+
});
|
|
7643
8390
|
this.pi.on("session_shutdown", async (event, ctx) => {
|
|
7644
8391
|
this.latestContext = ctx;
|
|
7645
8392
|
if (event.reason === "quit" && (ctx.mode === "print" || ctx.mode === "json")) {
|
|
@@ -7647,6 +8394,14 @@ var ShadowMindRuntime = class {
|
|
|
7647
8394
|
}
|
|
7648
8395
|
this.epoch += 1;
|
|
7649
8396
|
this.abortAll("session-shutdown");
|
|
8397
|
+
if (this.active.size > 0) {
|
|
8398
|
+
const result = await waitForSettled({
|
|
8399
|
+
timeoutMs: SESSION_TEARDOWN_TIMEOUT_MS,
|
|
8400
|
+
isSettled: () => this.active.size === 0
|
|
8401
|
+
});
|
|
8402
|
+
if (!result.settled) this.active.clear();
|
|
8403
|
+
}
|
|
8404
|
+
await this.usageStore.flush();
|
|
7650
8405
|
ctx.ui.setStatus("shadow-mind", void 0);
|
|
7651
8406
|
ctx.ui.setWidget("shadow-mind-panel", void 0);
|
|
7652
8407
|
this.sessionLifetime.deactivate();
|
|
@@ -7672,7 +8427,10 @@ var ShadowMindRuntime = class {
|
|
|
7672
8427
|
}
|
|
7673
8428
|
if (command === "status") {
|
|
7674
8429
|
await this.refresh(ctx);
|
|
7675
|
-
ctx.ui.notify(
|
|
8430
|
+
ctx.ui.notify(
|
|
8431
|
+
this.statusLines().join("\n"),
|
|
8432
|
+
this.diagnostics.length ? "warning" : "info"
|
|
8433
|
+
);
|
|
7676
8434
|
} else if (command === "hide") {
|
|
7677
8435
|
this.panelVisible = false;
|
|
7678
8436
|
ctx.ui.setWidget("shadow-mind-panel", void 0);
|
|
@@ -7690,25 +8448,36 @@ var ShadowMindRuntime = class {
|
|
|
7690
8448
|
this.setPaused(!this.paused, ctx);
|
|
7691
8449
|
}
|
|
7692
8450
|
});
|
|
7693
|
-
this.pi.registerMessageRenderer(
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
8451
|
+
this.pi.registerMessageRenderer(
|
|
8452
|
+
"shadow-report",
|
|
8453
|
+
(message, _options, theme) => {
|
|
8454
|
+
const content = typeof message.content === "string" ? message.content : "Shadow report";
|
|
8455
|
+
const prefix = theme.fg("accent", "\u{1F419} shadow \xB7 ");
|
|
8456
|
+
return new Text(`${prefix}${content}`, 0, 0);
|
|
8457
|
+
}
|
|
8458
|
+
);
|
|
7698
8459
|
}
|
|
7699
8460
|
async onHeartbeat(ctx) {
|
|
7700
8461
|
await this.refresh(ctx);
|
|
7701
8462
|
if (this.paused || !ctx.model) {
|
|
7702
|
-
this.record("heartbeat-skipped", {
|
|
8463
|
+
this.record("heartbeat-skipped", {
|
|
8464
|
+
reason: this.paused ? "paused" : "no-model",
|
|
8465
|
+
modelCalls: this.modelCalls
|
|
8466
|
+
});
|
|
7703
8467
|
return;
|
|
7704
8468
|
}
|
|
7705
8469
|
const snapshot = await this.registry.load();
|
|
7706
8470
|
const fullModelId = `${ctx.model.provider}/${ctx.model.id}`;
|
|
7707
8471
|
const decision = decideHeartbeat({
|
|
7708
8472
|
heartbeatProbability: this.configStore.current.heartbeatProbability,
|
|
7709
|
-
availableSlots: Math.max(
|
|
8473
|
+
availableSlots: Math.max(
|
|
8474
|
+
0,
|
|
8475
|
+
this.configStore.current.maxParallelShadows - this.active.size
|
|
8476
|
+
),
|
|
7710
8477
|
shadows: snapshot.shadows,
|
|
7711
|
-
activeShadowIds: new Set(
|
|
8478
|
+
activeShadowIds: new Set(
|
|
8479
|
+
[...this.active.values()].map(({ shadow }) => shadow.id)
|
|
8480
|
+
),
|
|
7712
8481
|
mainModelId: fullModelId,
|
|
7713
8482
|
random: this.random
|
|
7714
8483
|
});
|
|
@@ -7716,19 +8485,76 @@ var ShadowMindRuntime = class {
|
|
|
7716
8485
|
modelCalls: this.modelCalls,
|
|
7717
8486
|
roll: decision.heartbeatRoll,
|
|
7718
8487
|
candidates: decision.candidates,
|
|
7719
|
-
activated: decision.activated.map(({ shadow, roll }) => ({
|
|
8488
|
+
activated: decision.activated.map(({ shadow, roll }) => ({
|
|
8489
|
+
id: shadow.id,
|
|
8490
|
+
roll
|
|
8491
|
+
})),
|
|
7720
8492
|
...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
|
|
7721
8493
|
...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
|
|
7722
8494
|
});
|
|
7723
8495
|
if (!decision.activated.length) return;
|
|
7724
|
-
const context = buildSessionContext(
|
|
7725
|
-
|
|
8496
|
+
const context = buildSessionContext(
|
|
8497
|
+
ctx.sessionManager.getEntries(),
|
|
8498
|
+
ctx.sessionManager.getLeafId()
|
|
8499
|
+
);
|
|
8500
|
+
const availableTools = new Set(
|
|
8501
|
+
this.pi.getAllTools().map((tool2) => tool2.name)
|
|
8502
|
+
);
|
|
7726
8503
|
for (const { shadow } of decision.activated) {
|
|
7727
|
-
this.launchShadow(
|
|
8504
|
+
this.launchShadow(
|
|
8505
|
+
ctx,
|
|
8506
|
+
shadow,
|
|
8507
|
+
ctx.model,
|
|
8508
|
+
fullModelId,
|
|
8509
|
+
context,
|
|
8510
|
+
availableTools
|
|
8511
|
+
);
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
8514
|
+
async onFinalResponse(ctx) {
|
|
8515
|
+
await this.refresh(ctx);
|
|
8516
|
+
if (this.paused || !ctx.model) {
|
|
8517
|
+
this.record("final-response-skipped", {
|
|
8518
|
+
reason: this.paused ? "paused" : "no-model"
|
|
8519
|
+
});
|
|
8520
|
+
return;
|
|
7728
8521
|
}
|
|
8522
|
+
const snapshot = await this.registry.load();
|
|
8523
|
+
const mainModel = ctx.model;
|
|
8524
|
+
const fullModelId = `${mainModel.provider}/${mainModel.id}`;
|
|
8525
|
+
const decision = decideFinalResponse({
|
|
8526
|
+
shadows: snapshot.shadows,
|
|
8527
|
+
mainModelId: fullModelId
|
|
8528
|
+
});
|
|
8529
|
+
this.record("final-response", {
|
|
8530
|
+
candidates: decision.candidates,
|
|
8531
|
+
activated: decision.activated.map(({ shadow }) => shadow.id),
|
|
8532
|
+
...decision.modelFiltered.length ? { modelFiltered: decision.modelFiltered } : {},
|
|
8533
|
+
...decision.runningExcluded.length ? { runningExcluded: decision.runningExcluded } : {}
|
|
8534
|
+
});
|
|
8535
|
+
if (!decision.activated.length) return;
|
|
8536
|
+
const context = buildSessionContext(
|
|
8537
|
+
ctx.sessionManager.getEntries(),
|
|
8538
|
+
ctx.sessionManager.getLeafId()
|
|
8539
|
+
);
|
|
8540
|
+
const availableTools = new Set(
|
|
8541
|
+
this.pi.getAllTools().map((tool2) => tool2.name)
|
|
8542
|
+
);
|
|
8543
|
+
this.finalResponseQueue.enqueue(
|
|
8544
|
+
decision.activated.map(({ shadow }) => ({
|
|
8545
|
+
epoch: this.epoch,
|
|
8546
|
+
shadowId: shadow.id,
|
|
8547
|
+
ctx,
|
|
8548
|
+
shadow,
|
|
8549
|
+
mainModel,
|
|
8550
|
+
fullModelId,
|
|
8551
|
+
context,
|
|
8552
|
+
availableTools
|
|
8553
|
+
}))
|
|
8554
|
+
);
|
|
7729
8555
|
}
|
|
7730
8556
|
launchShadow(ctx, shadow, mainModel, fullModelId, context, availableTools) {
|
|
7731
|
-
const runId =
|
|
8557
|
+
const runId = randomUUID3();
|
|
7732
8558
|
const runEpoch = this.epoch;
|
|
7733
8559
|
const { tools, missing } = resolveShadowTools(shadow.tools, availableTools);
|
|
7734
8560
|
this.active.set(runId, { shadow, epoch: runEpoch });
|
|
@@ -7747,6 +8573,7 @@ var ShadowMindRuntime = class {
|
|
|
7747
8573
|
cwd: ctx.cwd,
|
|
7748
8574
|
agentDir: this.agentDir,
|
|
7749
8575
|
mainSystemPrompt: ctx.getSystemPrompt(),
|
|
8576
|
+
// SAFETY: buildSessionContext returns Pi message objects; the runner treats them as read-only generic records.
|
|
7750
8577
|
messages: context.messages,
|
|
7751
8578
|
mainModel,
|
|
7752
8579
|
tools,
|
|
@@ -7762,12 +8589,29 @@ var ShadowMindRuntime = class {
|
|
|
7762
8589
|
reason: "error",
|
|
7763
8590
|
error: error instanceof Error ? error.message : String(error)
|
|
7764
8591
|
});
|
|
8592
|
+
this.finalResponseQueue.slotAvailable();
|
|
7765
8593
|
});
|
|
7766
8594
|
}
|
|
7767
8595
|
handleRunEnd(runId, shadow, result) {
|
|
8596
|
+
const activeRun = this.active.get(runId);
|
|
7768
8597
|
this.active.delete(runId);
|
|
8598
|
+
const persisted = this.usageStore.add(result.usage);
|
|
8599
|
+
if (activeRun?.epoch !== this.epoch) return;
|
|
8600
|
+
this.sessionUsage = addUsage(this.sessionUsage, result.usage);
|
|
8601
|
+
this.recentRuns.push({
|
|
8602
|
+
shadowName: shadow.name,
|
|
8603
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8604
|
+
result
|
|
8605
|
+
});
|
|
8606
|
+
if (this.recentRuns.length > 5) this.recentRuns.shift();
|
|
7769
8607
|
this.record("run-end", { runId, shadowId: shadow.id, ...result });
|
|
7770
|
-
if (this.latestContext && this.sessionLifetime.isActive)
|
|
8608
|
+
if (this.latestContext && this.sessionLifetime.isActive)
|
|
8609
|
+
this.updateStatus(this.latestContext);
|
|
8610
|
+
this.finalResponseQueue.slotAvailable();
|
|
8611
|
+
void persisted.then(() => {
|
|
8612
|
+
if (this.latestContext && this.sessionLifetime.isActive)
|
|
8613
|
+
this.updateStatus(this.latestContext);
|
|
8614
|
+
});
|
|
7771
8615
|
}
|
|
7772
8616
|
acceptReport(report) {
|
|
7773
8617
|
if (report.epoch !== this.epoch) return;
|
|
@@ -7777,15 +8621,26 @@ var ShadowMindRuntime = class {
|
|
|
7777
8621
|
const current = reports.filter((report) => report.epoch === this.epoch);
|
|
7778
8622
|
if (!current.length) return;
|
|
7779
8623
|
const content = formatReportBatch(current);
|
|
7780
|
-
this.record("report-delivered", {
|
|
8624
|
+
this.record("report-delivered", {
|
|
8625
|
+
runIds: current.map((report) => report.runId),
|
|
8626
|
+
count: current.length
|
|
8627
|
+
});
|
|
7781
8628
|
const idle = this.latestContext?.isIdle() ?? true;
|
|
7782
8629
|
this.sessionLifetime.run(() => {
|
|
7783
|
-
this.pi.sendMessage(
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
8630
|
+
this.pi.sendMessage(
|
|
8631
|
+
{
|
|
8632
|
+
customType: "shadow-report",
|
|
8633
|
+
content,
|
|
8634
|
+
display: true,
|
|
8635
|
+
details: {
|
|
8636
|
+
reports: current.map(({ shadowId, runId }) => ({
|
|
8637
|
+
shadowId,
|
|
8638
|
+
runId
|
|
8639
|
+
}))
|
|
8640
|
+
}
|
|
8641
|
+
},
|
|
8642
|
+
{ triggerTurn: true, deliverAs: idle ? "followUp" : "steer" }
|
|
8643
|
+
);
|
|
7789
8644
|
});
|
|
7790
8645
|
}
|
|
7791
8646
|
async refresh(ctx) {
|
|
@@ -7795,57 +8650,93 @@ var ShadowMindRuntime = class {
|
|
|
7795
8650
|
this.shadowCount = registry.shadows.length;
|
|
7796
8651
|
this.diagnostics = [
|
|
7797
8652
|
...config.error ? [`config: ${config.error}`] : [],
|
|
7798
|
-
...registry.diagnostics.map(
|
|
8653
|
+
...registry.diagnostics.map(
|
|
8654
|
+
(item) => `${item.filePath}: ${item.message}`
|
|
8655
|
+
)
|
|
7799
8656
|
];
|
|
7800
8657
|
this.updateStatus(ctx);
|
|
7801
8658
|
}
|
|
7802
8659
|
abortAll(reason) {
|
|
7803
8660
|
this.runner.abortAll();
|
|
8661
|
+
this.finalResponseQueue.clear();
|
|
7804
8662
|
this.batcher.clear();
|
|
7805
8663
|
this.record("runs-aborted", { reason, count: this.active.size });
|
|
7806
8664
|
}
|
|
7807
8665
|
async drainHeadless(ctx) {
|
|
7808
8666
|
if (this.active.size === 0 && !this.batcher.hasPending) return;
|
|
7809
8667
|
const timeoutMs = this.configStore.current.headlessDrainTimeoutSeconds * 1e3;
|
|
7810
|
-
this.record("headless-drain-start", {
|
|
8668
|
+
this.record("headless-drain-start", {
|
|
8669
|
+
timeoutMs,
|
|
8670
|
+
active: this.active.size
|
|
8671
|
+
});
|
|
7811
8672
|
const result = await waitForSettled({
|
|
7812
8673
|
timeoutMs,
|
|
7813
8674
|
isSettled: () => this.active.size === 0 && !this.batcher.hasPending && ctx.isIdle() && !ctx.hasPendingMessages()
|
|
7814
8675
|
});
|
|
7815
|
-
this.record(
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
8676
|
+
this.record(
|
|
8677
|
+
result.settled ? "headless-drain-complete" : "headless-drain-timeout",
|
|
8678
|
+
{
|
|
8679
|
+
durationMs: result.durationMs,
|
|
8680
|
+
active: this.active.size
|
|
8681
|
+
}
|
|
8682
|
+
);
|
|
7819
8683
|
if (!result.settled) this.abortAll("headless-drain-timeout");
|
|
7820
8684
|
}
|
|
7821
8685
|
record(kind, data) {
|
|
7822
|
-
const event = {
|
|
8686
|
+
const event = {
|
|
8687
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8688
|
+
kind,
|
|
8689
|
+
epoch: this.epoch,
|
|
8690
|
+
data
|
|
8691
|
+
};
|
|
7823
8692
|
this.recentEvents.push(event);
|
|
7824
8693
|
if (this.recentEvents.length > 20) this.recentEvents.shift();
|
|
7825
|
-
this.sessionLifetime.run(
|
|
8694
|
+
this.sessionLifetime.run(
|
|
8695
|
+
() => this.pi.appendEntry("shadow-mind-event", event)
|
|
8696
|
+
);
|
|
7826
8697
|
}
|
|
7827
8698
|
setPaused(paused, ctx) {
|
|
7828
8699
|
this.paused = paused;
|
|
7829
8700
|
if (paused) this.abortAll("paused");
|
|
7830
|
-
ctx.ui.notify(
|
|
8701
|
+
ctx.ui.notify(
|
|
8702
|
+
paused ? "Shadow Mind paused" : "Shadow Mind resumed",
|
|
8703
|
+
"info"
|
|
8704
|
+
);
|
|
7831
8705
|
this.updateStatus(ctx);
|
|
7832
8706
|
}
|
|
7833
8707
|
updateStatus(ctx) {
|
|
7834
8708
|
this.sessionLifetime.run(() => {
|
|
7835
|
-
const
|
|
7836
|
-
|
|
7837
|
-
|
|
8709
|
+
const diagnostics = this.usageStore.error ? [...this.diagnostics, `usage: ${this.usageStore.error}`] : this.diagnostics;
|
|
8710
|
+
const warning = diagnostics.length || this.hasRecentRunErrors() ? " !" : "";
|
|
8711
|
+
const usage = `${formatUsageTokens(this.sessionUsage.totalTokens)} \xB7 ${formatUsageCost(this.sessionUsage.cost.total)}`;
|
|
8712
|
+
ctx.ui.setStatus(
|
|
8713
|
+
"shadow-mind",
|
|
8714
|
+
this.paused ? `\u{1F419} Paused \xB7 ${usage}${warning}` : `\u{1F419} ${this.active.size} \xB7 ${usage}${warning}`
|
|
8715
|
+
);
|
|
8716
|
+
if (this.panelVisible)
|
|
8717
|
+
ctx.ui.setWidget("shadow-mind-panel", this.statusLines(), {
|
|
8718
|
+
placement: "aboveEditor"
|
|
8719
|
+
});
|
|
7838
8720
|
});
|
|
7839
8721
|
}
|
|
7840
8722
|
hasRecentRunErrors() {
|
|
7841
|
-
return this.recentEvents.slice(-3).some(
|
|
8723
|
+
return this.recentEvents.slice(-3).some(
|
|
8724
|
+
(event) => event.kind === "run-end" && (event.data?.reason === "error" || event.data?.reason === "timeout")
|
|
8725
|
+
);
|
|
7842
8726
|
}
|
|
7843
8727
|
statusLines() {
|
|
7844
8728
|
const config = this.configStore.current;
|
|
8729
|
+
const diagnostics = this.usageStore.error ? [...this.diagnostics, `usage: ${this.usageStore.error}`] : this.diagnostics;
|
|
7845
8730
|
return [
|
|
7846
8731
|
`\u{1F419} Shadow Mind \xB7 ${this.paused ? "paused" : "active"} \xB7 running ${this.active.size}/${config.maxParallelShadows}`,
|
|
7847
8732
|
`heartbeat ${formatNumber(config.heartbeatProbability)} \xB7 batch ${config.resultBatchWindowMs}ms \xB7 timeout ${config.defaultShadowTimeoutSeconds}s \xB7 drain ${config.headlessDrainTimeoutSeconds}s \xB7 thinking ${config.defaultThinkingLevel}`,
|
|
7848
8733
|
`definitions: ${this.shadowCount} valid \xB7 ${this.diagnostics.length} invalid`,
|
|
8734
|
+
formatUsageDetail("session", this.sessionUsage),
|
|
8735
|
+
`usage lifetime \xB7 ${formatUsageSummary(this.usageStore.current)}`,
|
|
8736
|
+
...this.recentRuns.slice(-3).map(
|
|
8737
|
+
({ shadowName, completedAt, result }) => `recent run \xB7 ${formatCompletedAt(completedAt)} \xB7 ${shadowName} \xB7 ${result.reason} \xB7 ${formatUsageSummary(result.usage)}`
|
|
8738
|
+
),
|
|
8739
|
+
...diagnostics.map((diagnostic) => `diagnostic: ${diagnostic}`),
|
|
7849
8740
|
...this.recentEvents.slice(-5).map((event) => {
|
|
7850
8741
|
const failed = event.kind === "run-end" && (event.data?.reason === "error" || event.data?.reason === "timeout");
|
|
7851
8742
|
const detail = failed ? ` ${event.data?.error ?? event.data?.reason}` : "";
|
|
@@ -7858,11 +8749,22 @@ var ShadowMindRuntime = class {
|
|
|
7858
8749
|
function resolveModel(ctx, fullId) {
|
|
7859
8750
|
const separator = fullId.indexOf("/");
|
|
7860
8751
|
if (separator <= 0 || separator === fullId.length - 1) return void 0;
|
|
7861
|
-
return ctx.modelRegistry.find(
|
|
8752
|
+
return ctx.modelRegistry.find(
|
|
8753
|
+
fullId.slice(0, separator),
|
|
8754
|
+
fullId.slice(separator + 1)
|
|
8755
|
+
);
|
|
7862
8756
|
}
|
|
7863
8757
|
function formatNumber(value) {
|
|
7864
8758
|
return Number(value.toFixed(3)).toString();
|
|
7865
8759
|
}
|
|
8760
|
+
function formatCompletedAt(completedAt) {
|
|
8761
|
+
return new Date(completedAt).toLocaleTimeString("en-GB", {
|
|
8762
|
+
hour: "2-digit",
|
|
8763
|
+
minute: "2-digit",
|
|
8764
|
+
second: "2-digit",
|
|
8765
|
+
hourCycle: "h23"
|
|
8766
|
+
});
|
|
8767
|
+
}
|
|
7866
8768
|
|
|
7867
8769
|
// src/index.ts
|
|
7868
8770
|
function shadowMindExtension(pi) {
|