topchester-ai 0.18.0 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +587 -59
- package/dist/cli.mjs.map +1 -1
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -2212,7 +2212,7 @@ function killChild(pid, detached, signal) {
|
|
|
2212
2212
|
const inspectCommandTool = defineTool({
|
|
2213
2213
|
name: "inspect_command",
|
|
2214
2214
|
description: "Run a narrowly validated read-only command for repository orientation.",
|
|
2215
|
-
prompt: "inspect_command: run a safe read-only discovery command inside the workspace for quick orientation; prefer read_file, list_files, grep, and find_file for exact file tasks, and do not use it for builds, tests, installs, network, shell scripts, or
|
|
2215
|
+
prompt: "inspect_command: run a safe read-only discovery command inside the workspace for quick repo orientation; prefer read_file, list_files, grep, and find_file for exact file tasks, and do not use it for builds, tests, installs, network, shell scripts, edits, or user-requested specific commands such as node --version, which node, or pnpm --version. To use it, reply with only JSON: {\"tool\":\"inspect_command\",\"args\":{\"command\":\"pwd && rg --files docs/plans | head -20\",\"workdir\":\".\",\"timeout_ms\":10000}}",
|
|
2216
2216
|
argsSchema: inspectCommandArgsSchema,
|
|
2217
2217
|
execute: (context, args) => inspectWorkspaceCommand(context.workspaceRoot, args, { pathEnv: context.pathEnv })
|
|
2218
2218
|
});
|
|
@@ -3259,8 +3259,8 @@ const runCommandArgsSchema = z.object({
|
|
|
3259
3259
|
});
|
|
3260
3260
|
const runCommandTool = defineTool({
|
|
3261
3261
|
name: "run_command",
|
|
3262
|
-
description: "Run a
|
|
3263
|
-
prompt: "run_command: run a
|
|
3262
|
+
description: "Run a policy-gated command inside the workspace.",
|
|
3263
|
+
prompt: "run_command: run a user-requested command when no more specific tool fits; it is the general policy-gated command runner, and runtime policy decides whether the command is allowed, rejected, or needs approval. Prefer run_validator for tests, lint, typecheck, build, check, format-check, and smoke. To use it, reply with only JSON: {\"tool\":\"run_command\",\"args\":{\"command\":\"node --version\",\"workdir\":\".\",\"timeout_ms\":30000}}",
|
|
3264
3264
|
argsSchema: runCommandArgsSchema,
|
|
3265
3265
|
requiresExclusiveWorkspace: true,
|
|
3266
3266
|
execute: async (context, args) => runWorkspaceCommand(context.workspaceRoot, args, context.config?.tools?.commands, context.runCommandApprovals?.allowExactCommands, context.pathEnv, context.abortSignal)
|
|
@@ -4452,6 +4452,45 @@ const commandPolicySchema = z.object({
|
|
|
4452
4452
|
allowExact: z.array(commandPatternSchema).optional().default([]),
|
|
4453
4453
|
deny: z.array(commandPatternSchema).optional().default([])
|
|
4454
4454
|
}).strict();
|
|
4455
|
+
const hookEventNames = [
|
|
4456
|
+
"SessionStart",
|
|
4457
|
+
"UserPromptSubmit",
|
|
4458
|
+
"PreToolUse",
|
|
4459
|
+
"PostToolUse",
|
|
4460
|
+
"PermissionRequest",
|
|
4461
|
+
"PreCompact",
|
|
4462
|
+
"Stop"
|
|
4463
|
+
];
|
|
4464
|
+
const hookEventAliasMap = {
|
|
4465
|
+
TaskStart: "SessionStart",
|
|
4466
|
+
TaskAcknowledge: "UserPromptSubmit",
|
|
4467
|
+
UserActionRequired: "PermissionRequest",
|
|
4468
|
+
TaskComplete: "Stop"
|
|
4469
|
+
};
|
|
4470
|
+
const hookTimeoutMsSchema = z.number().int().positive().max(6e5);
|
|
4471
|
+
const hookMatcherSchema = z.union([z.string().min(1), z.array(z.string().min(1))]).optional();
|
|
4472
|
+
const hookHandlerSchema = z.object({
|
|
4473
|
+
type: z.literal("command").optional(),
|
|
4474
|
+
command: z.string().min(1),
|
|
4475
|
+
timeoutMs: hookTimeoutMsSchema.optional(),
|
|
4476
|
+
matcher: hookMatcherSchema
|
|
4477
|
+
}).strict();
|
|
4478
|
+
const canonicalHooksConfigSchema = z.object({
|
|
4479
|
+
enabled: z.boolean().optional(),
|
|
4480
|
+
SessionStart: z.array(hookHandlerSchema).optional(),
|
|
4481
|
+
UserPromptSubmit: z.array(hookHandlerSchema).optional(),
|
|
4482
|
+
PreToolUse: z.array(hookHandlerSchema).optional(),
|
|
4483
|
+
PostToolUse: z.array(hookHandlerSchema).optional(),
|
|
4484
|
+
PermissionRequest: z.array(hookHandlerSchema).optional(),
|
|
4485
|
+
PreCompact: z.array(hookHandlerSchema).optional(),
|
|
4486
|
+
Stop: z.array(hookHandlerSchema).optional()
|
|
4487
|
+
}).strict();
|
|
4488
|
+
const rawHooksConfigSchema = canonicalHooksConfigSchema.extend({
|
|
4489
|
+
TaskStart: z.array(hookHandlerSchema).optional(),
|
|
4490
|
+
TaskAcknowledge: z.array(hookHandlerSchema).optional(),
|
|
4491
|
+
UserActionRequired: z.array(hookHandlerSchema).optional(),
|
|
4492
|
+
TaskComplete: z.array(hookHandlerSchema).optional()
|
|
4493
|
+
}).strict();
|
|
4455
4494
|
const topchesterConfigSchema = z.object({
|
|
4456
4495
|
models: z.object({
|
|
4457
4496
|
defaultPurpose: modelPurposeSchema.optional(),
|
|
@@ -4460,12 +4499,14 @@ const topchesterConfigSchema = z.object({
|
|
|
4460
4499
|
providers: providersSchema.optional()
|
|
4461
4500
|
}).strict().optional(),
|
|
4462
4501
|
ignore: z.object({ paths: z.array(ignorePathSchema).optional() }).optional(),
|
|
4463
|
-
tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional()
|
|
4502
|
+
tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional(),
|
|
4503
|
+
hooks: canonicalHooksConfigSchema.optional()
|
|
4464
4504
|
});
|
|
4465
4505
|
const rawTopchesterConfigSchema = z.object({
|
|
4466
4506
|
models: rawModelsSchema.optional(),
|
|
4467
4507
|
ignore: z.object({ paths: z.array(ignorePathSchema).optional() }).optional(),
|
|
4468
|
-
tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional()
|
|
4508
|
+
tools: z.object({ commands: commandPolicySchema.optional() }).strict().optional(),
|
|
4509
|
+
hooks: rawHooksConfigSchema.optional()
|
|
4469
4510
|
});
|
|
4470
4511
|
function getGlobalTopchesterConfigDir() {
|
|
4471
4512
|
return join(homedir(), ".config", "topchester");
|
|
@@ -4644,6 +4685,9 @@ function parseConfigFile(path, value) {
|
|
|
4644
4685
|
return parsed.data;
|
|
4645
4686
|
}
|
|
4646
4687
|
function normalizeConfigInput(value) {
|
|
4688
|
+
return normalizeHooksConfigInput(normalizeModelsConfigInput(value));
|
|
4689
|
+
}
|
|
4690
|
+
function normalizeModelsConfigInput(value) {
|
|
4647
4691
|
if (!isPlainObject(value) || !isPlainObject(value.models)) return value;
|
|
4648
4692
|
const models = { ...value.models };
|
|
4649
4693
|
const providers = isPlainObject(models.providers) ? { ...models.providers } : {};
|
|
@@ -4683,6 +4727,22 @@ function normalizeConfigInput(value) {
|
|
|
4683
4727
|
}
|
|
4684
4728
|
};
|
|
4685
4729
|
}
|
|
4730
|
+
function normalizeHooksConfigInput(value) {
|
|
4731
|
+
if (!isPlainObject(value) || !isPlainObject(value.hooks)) return value;
|
|
4732
|
+
const hooks = { ...value.hooks };
|
|
4733
|
+
for (const [alias, canonical] of Object.entries(hookEventAliasMap)) {
|
|
4734
|
+
const aliasHandlers = hooks[alias];
|
|
4735
|
+
if (aliasHandlers === void 0) continue;
|
|
4736
|
+
const canonicalHandlers = hooks[canonical];
|
|
4737
|
+
if (!Array.isArray(aliasHandlers) || canonicalHandlers !== void 0 && !Array.isArray(canonicalHandlers)) continue;
|
|
4738
|
+
hooks[canonical] = [...canonicalHandlers ?? [], ...aliasHandlers];
|
|
4739
|
+
delete hooks[alias];
|
|
4740
|
+
}
|
|
4741
|
+
return {
|
|
4742
|
+
...value,
|
|
4743
|
+
hooks
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4686
4746
|
function normalizeModelRef(ref, defaultProvider) {
|
|
4687
4747
|
if (typeof ref === "string") return parseModelRef(ref, defaultProvider);
|
|
4688
4748
|
if (!isPlainObject(ref) || typeof ref.name !== "string") return;
|
|
@@ -4745,7 +4805,7 @@ function isOpenAIProvider(providerId, baseURL) {
|
|
|
4745
4805
|
function deepMerge(base, override, path = []) {
|
|
4746
4806
|
if (Array.isArray(base) && Array.isArray(override)) {
|
|
4747
4807
|
const joinedPath = path.join(".");
|
|
4748
|
-
return joinedPath === "ignore.paths" || joinedPath === "tools.commands.allow" || joinedPath === "tools.commands.allowExact" || joinedPath === "tools.commands.deny" ? [...base, ...override] : override;
|
|
4808
|
+
return joinedPath === "ignore.paths" || joinedPath === "tools.commands.allow" || joinedPath === "tools.commands.allowExact" || joinedPath === "tools.commands.deny" || path.length === 2 && path[0] === "hooks" && hookEventNames.includes(path[1]) ? [...base, ...override] : override;
|
|
4749
4809
|
}
|
|
4750
4810
|
if (!isPlainObject(base) || !isPlainObject(override)) return override;
|
|
4751
4811
|
const result = { ...base };
|
|
@@ -8629,6 +8689,225 @@ function isAbortError(error) {
|
|
|
8629
8689
|
return error.name === "AbortError" || error.message.toLowerCase().includes("aborted");
|
|
8630
8690
|
}
|
|
8631
8691
|
//#endregion
|
|
8692
|
+
//#region src/agent/hooks.ts
|
|
8693
|
+
const DEFAULT_HOOK_TIMEOUT_MS = 5e3;
|
|
8694
|
+
const MAX_CAPTURED_OUTPUT_CHARS = 64e3;
|
|
8695
|
+
const hookResponseSchema = z.object({
|
|
8696
|
+
action: z.enum([
|
|
8697
|
+
"continue",
|
|
8698
|
+
"block",
|
|
8699
|
+
"stop"
|
|
8700
|
+
]).optional(),
|
|
8701
|
+
decision: z.string().optional(),
|
|
8702
|
+
cancel: z.boolean().optional(),
|
|
8703
|
+
context: z.union([z.string(), z.array(z.string())]).optional(),
|
|
8704
|
+
message: z.string().optional(),
|
|
8705
|
+
feedback: z.string().optional(),
|
|
8706
|
+
reason: z.string().optional()
|
|
8707
|
+
}).passthrough();
|
|
8708
|
+
async function runTopchesterHooks(context, event, payload, options = {}) {
|
|
8709
|
+
const handlers = getConfiguredHookHandlers(context, event, options.toolName);
|
|
8710
|
+
const result = {
|
|
8711
|
+
contexts: [],
|
|
8712
|
+
messages: [],
|
|
8713
|
+
handlerCount: 0
|
|
8714
|
+
};
|
|
8715
|
+
for (const handler of handlers) {
|
|
8716
|
+
result.handlerCount += 1;
|
|
8717
|
+
const handlerResult = await runCommandHandler(context, event, payload, handler, options);
|
|
8718
|
+
result.contexts.push(...handlerResult.contexts);
|
|
8719
|
+
result.messages.push(...handlerResult.messages);
|
|
8720
|
+
if (handlerResult.blocked) {
|
|
8721
|
+
result.blocked = handlerResult.blocked;
|
|
8722
|
+
break;
|
|
8723
|
+
}
|
|
8724
|
+
if (handlerResult.stopped) {
|
|
8725
|
+
result.stopped = handlerResult.stopped;
|
|
8726
|
+
break;
|
|
8727
|
+
}
|
|
8728
|
+
}
|
|
8729
|
+
return result;
|
|
8730
|
+
}
|
|
8731
|
+
function formatHookContextsForPrompt(event, contexts) {
|
|
8732
|
+
const normalized = contexts.map((context) => context.trim()).filter(Boolean);
|
|
8733
|
+
if (normalized.length === 0) return "";
|
|
8734
|
+
return [`Hook context from ${event}:`, ...normalized].join("\n\n");
|
|
8735
|
+
}
|
|
8736
|
+
function getConfiguredHookHandlers(context, event, toolName) {
|
|
8737
|
+
const hooks = context.config.hooks;
|
|
8738
|
+
if (!hooks || hooks.enabled === false) return [];
|
|
8739
|
+
return (hooks[event] ?? []).filter((handler) => hookMatches(handler, event, toolName));
|
|
8740
|
+
}
|
|
8741
|
+
function hookMatches(handler, event, toolName) {
|
|
8742
|
+
const matcher = handler.matcher;
|
|
8743
|
+
if (matcher === void 0) return true;
|
|
8744
|
+
const target = toolName ?? event;
|
|
8745
|
+
return (Array.isArray(matcher) ? matcher : [matcher]).some((entry) => entry === "*" || entry === target);
|
|
8746
|
+
}
|
|
8747
|
+
async function runCommandHandler(context, event, payload, handler, options) {
|
|
8748
|
+
const result = await runHookProcess(handler.command ?? "", payload, {
|
|
8749
|
+
cwd: context.workspaceRoot,
|
|
8750
|
+
timeoutMs: handler.timeoutMs ?? DEFAULT_HOOK_TIMEOUT_MS,
|
|
8751
|
+
abortSignal: options.abortSignal,
|
|
8752
|
+
env: buildHookEnv(event, options.toolName)
|
|
8753
|
+
});
|
|
8754
|
+
logHookProcessResult(context, event, handler, result);
|
|
8755
|
+
if (result.timedOut || result.aborted || result.spawnError || result.exitCode !== 0) return emptyHookRunResult();
|
|
8756
|
+
const stdout = result.stdout.trim();
|
|
8757
|
+
if (!stdout) return emptyHookRunResult();
|
|
8758
|
+
let parsed;
|
|
8759
|
+
try {
|
|
8760
|
+
parsed = JSON.parse(stdout);
|
|
8761
|
+
} catch (error) {
|
|
8762
|
+
logHookWarning(context, {
|
|
8763
|
+
event: "hook_response_parse_failed",
|
|
8764
|
+
hookEventName: event,
|
|
8765
|
+
error: error instanceof Error ? error.message : String(error),
|
|
8766
|
+
stdoutLength: result.stdout.length
|
|
8767
|
+
});
|
|
8768
|
+
return emptyHookRunResult();
|
|
8769
|
+
}
|
|
8770
|
+
const response = hookResponseSchema.safeParse(parsed);
|
|
8771
|
+
if (!response.success) {
|
|
8772
|
+
logHookWarning(context, {
|
|
8773
|
+
event: "hook_response_invalid",
|
|
8774
|
+
hookEventName: event,
|
|
8775
|
+
issues: response.error.issues.map((issue) => issue.message)
|
|
8776
|
+
});
|
|
8777
|
+
return emptyHookRunResult();
|
|
8778
|
+
}
|
|
8779
|
+
return normalizeHookResponse(event, response.data);
|
|
8780
|
+
}
|
|
8781
|
+
function normalizeHookResponse(event, response) {
|
|
8782
|
+
const result = emptyHookRunResult();
|
|
8783
|
+
const action = normalizeHookAction(event, response);
|
|
8784
|
+
const message = firstNonEmpty(response.message, response.feedback, response.reason);
|
|
8785
|
+
const contexts = Array.isArray(response.context) ? response.context : response.context ? [response.context] : [];
|
|
8786
|
+
result.contexts.push(...contexts.filter((context) => context.trim().length > 0));
|
|
8787
|
+
if (message) result.messages.push(message);
|
|
8788
|
+
if (action === "block") result.blocked = { message: message || `Hook ${event} blocked the request.` };
|
|
8789
|
+
else if (action === "stop") result.stopped = { message: message || `Hook ${event} stopped the turn.` };
|
|
8790
|
+
return result;
|
|
8791
|
+
}
|
|
8792
|
+
function normalizeHookAction(event, response) {
|
|
8793
|
+
if (response.cancel) return event === "Stop" ? "stop" : "block";
|
|
8794
|
+
if (response.action) return response.action;
|
|
8795
|
+
const decision = response.decision?.trim().toLowerCase();
|
|
8796
|
+
if (decision === "block" || decision === "deny" || decision === "denied") return "block";
|
|
8797
|
+
if (decision === "stop" || decision === "halt") return "stop";
|
|
8798
|
+
return "continue";
|
|
8799
|
+
}
|
|
8800
|
+
function buildHookEnv(event, toolName) {
|
|
8801
|
+
return {
|
|
8802
|
+
...process.env,
|
|
8803
|
+
TOPCHESTER_HOOK_EVENT: event,
|
|
8804
|
+
TOPCHESTER_HOOK_TOOL: toolName ?? ""
|
|
8805
|
+
};
|
|
8806
|
+
}
|
|
8807
|
+
async function runHookProcess(command, payload, options) {
|
|
8808
|
+
const startedAt = Date.now();
|
|
8809
|
+
const shell = process.env.SHELL || "/bin/sh";
|
|
8810
|
+
return new Promise((resolve) => {
|
|
8811
|
+
let stdout = "";
|
|
8812
|
+
let stderr = "";
|
|
8813
|
+
let settled = false;
|
|
8814
|
+
let timedOut = false;
|
|
8815
|
+
let aborted = false;
|
|
8816
|
+
const child = spawn(shell, ["-lc", command], {
|
|
8817
|
+
cwd: options.cwd,
|
|
8818
|
+
env: options.env,
|
|
8819
|
+
stdio: [
|
|
8820
|
+
"pipe",
|
|
8821
|
+
"pipe",
|
|
8822
|
+
"pipe"
|
|
8823
|
+
]
|
|
8824
|
+
});
|
|
8825
|
+
const finish = (partial) => {
|
|
8826
|
+
if (settled) return;
|
|
8827
|
+
settled = true;
|
|
8828
|
+
clearTimeout(timeout);
|
|
8829
|
+
options.abortSignal?.removeEventListener("abort", abort);
|
|
8830
|
+
resolve({
|
|
8831
|
+
stdout,
|
|
8832
|
+
stderr,
|
|
8833
|
+
exitCode: null,
|
|
8834
|
+
signal: null,
|
|
8835
|
+
timedOut,
|
|
8836
|
+
aborted,
|
|
8837
|
+
durationMs: Date.now() - startedAt,
|
|
8838
|
+
...partial
|
|
8839
|
+
});
|
|
8840
|
+
};
|
|
8841
|
+
const timeout = setTimeout(() => {
|
|
8842
|
+
timedOut = true;
|
|
8843
|
+
child.kill("SIGTERM");
|
|
8844
|
+
}, options.timeoutMs);
|
|
8845
|
+
const abort = () => {
|
|
8846
|
+
aborted = true;
|
|
8847
|
+
child.kill("SIGTERM");
|
|
8848
|
+
};
|
|
8849
|
+
if (options.abortSignal?.aborted) abort();
|
|
8850
|
+
else options.abortSignal?.addEventListener("abort", abort, { once: true });
|
|
8851
|
+
child.stdout?.setEncoding("utf8");
|
|
8852
|
+
child.stderr?.setEncoding("utf8");
|
|
8853
|
+
child.stdout?.on("data", (chunk) => {
|
|
8854
|
+
stdout = appendCapped(stdout, chunk);
|
|
8855
|
+
});
|
|
8856
|
+
child.stderr?.on("data", (chunk) => {
|
|
8857
|
+
stderr = appendCapped(stderr, chunk);
|
|
8858
|
+
});
|
|
8859
|
+
child.on("error", (error) => {
|
|
8860
|
+
finish({ spawnError: error.message });
|
|
8861
|
+
});
|
|
8862
|
+
child.on("close", (exitCode, signal) => {
|
|
8863
|
+
finish({
|
|
8864
|
+
exitCode,
|
|
8865
|
+
signal
|
|
8866
|
+
});
|
|
8867
|
+
});
|
|
8868
|
+
child.stdin?.on("error", () => {});
|
|
8869
|
+
child.stdin?.end(`${JSON.stringify(payload)}\n`);
|
|
8870
|
+
});
|
|
8871
|
+
}
|
|
8872
|
+
function logHookProcessResult(context, event, handler, result) {
|
|
8873
|
+
context.logger.debug({
|
|
8874
|
+
event: "hook_run",
|
|
8875
|
+
hookEventName: event,
|
|
8876
|
+
handlerType: handler.type ?? "command",
|
|
8877
|
+
matcher: handler.matcher,
|
|
8878
|
+
exitCode: result.exitCode,
|
|
8879
|
+
signal: result.signal,
|
|
8880
|
+
timedOut: result.timedOut,
|
|
8881
|
+
aborted: result.aborted,
|
|
8882
|
+
spawnError: result.spawnError,
|
|
8883
|
+
durationMs: result.durationMs,
|
|
8884
|
+
stdoutLength: result.stdout.length,
|
|
8885
|
+
stderrLength: result.stderr.length
|
|
8886
|
+
}, "hook run");
|
|
8887
|
+
}
|
|
8888
|
+
function logHookWarning(context, payload) {
|
|
8889
|
+
const logger = context.logger;
|
|
8890
|
+
if (typeof logger.warn === "function") {
|
|
8891
|
+
logger.warn(payload, "hook warning");
|
|
8892
|
+
return;
|
|
8893
|
+
}
|
|
8894
|
+
context.logger.debug(payload, "hook warning");
|
|
8895
|
+
}
|
|
8896
|
+
function emptyHookRunResult() {
|
|
8897
|
+
return {
|
|
8898
|
+
contexts: [],
|
|
8899
|
+
messages: [],
|
|
8900
|
+
handlerCount: 0
|
|
8901
|
+
};
|
|
8902
|
+
}
|
|
8903
|
+
function appendCapped(current, chunk) {
|
|
8904
|
+
if (current.length >= MAX_CAPTURED_OUTPUT_CHARS) return current;
|
|
8905
|
+
return `${current}${chunk}`.slice(0, MAX_CAPTURED_OUTPUT_CHARS);
|
|
8906
|
+
}
|
|
8907
|
+
function firstNonEmpty(...values) {
|
|
8908
|
+
return values.find((value) => value !== void 0 && value.trim().length > 0);
|
|
8909
|
+
}
|
|
8910
|
+
//#endregion
|
|
8632
8911
|
//#region src/agent/profiles.ts
|
|
8633
8912
|
const READ_ONLY_TOOLS = [
|
|
8634
8913
|
"read_file",
|
|
@@ -8946,14 +9225,24 @@ function getChatSystemPrompt(options = {}) {
|
|
|
8946
9225
|
...canUseTool("list_files") && canUseTool("grep") && canUseTool("find_file") && canUseTool("read_file") ? ["- Use list_files, grep, find_file, and read_file for exact file listing, search, lookup, and reading tasks."] : [],
|
|
8947
9226
|
...canUseTool("git_status") && canUseTool("git_diff") && canUseTool("git_log") ? ["- Use git_status, git_diff, and git_log for Git state, diffs, and history. Prefer these over inspect_command for Git workflow inspection."] : [],
|
|
8948
9227
|
...canUseTool("git_add") && canUseTool("git_commit") ? ["- Use git_add and git_commit only when the user explicitly asks to stage or commit. Never stage unrelated files, never stage '.', and never commit unless staged paths exactly match the user's request."] : [],
|
|
8949
|
-
...canUseTool("inspect_command") ? [
|
|
9228
|
+
...canUseTool("inspect_command") ? [
|
|
9229
|
+
"- Use inspect_command only for quick read-only repo orientation when the user did not ask to run a specific command and a short familiar command chain is clearer than several dedicated tool calls.",
|
|
9230
|
+
"- inspect_command is not a shell. Unsafe commands, shell expansion, scripts, installs, builds, tests, network access, and file mutation are not available through it.",
|
|
9231
|
+
canUseTool("run_command") ? "- Do not use inspect_command when the user asks to run a specific command such as node --version, which node, or pnpm --version; use run_command or run_validator instead." : ""
|
|
9232
|
+
].filter(Boolean) : [],
|
|
8950
9233
|
...canUseTool("run_validator") ? [
|
|
8951
9234
|
"- After code edits, use run_validator when there is a relevant test, lint, typecheck, build, check, format-check, or smoke command that can prove the change.",
|
|
8952
9235
|
"- Failed run_validator exits are evidence. Read stdout and stderr, fix the issue when it is in scope, and rerun the narrowest useful validator.",
|
|
8953
9236
|
canUseTool("run_command") ? "- If run_validator is rejected because the command is not a strict validator shape but the user still needs command output, retry with run_command when project policy allows it." : "",
|
|
8954
9237
|
"- Do not use inspect_command for tests, builds, lint, typecheck, format checks, or smoke checks. Use run_validator for verification."
|
|
8955
9238
|
].filter(Boolean) : [],
|
|
8956
|
-
...canUseTool("run_command") ? [
|
|
9239
|
+
...canUseTool("run_command") ? [
|
|
9240
|
+
"- When the user explicitly asks to run a command or asks for command output, use run_command unless a more specific tool is clearly the right fit.",
|
|
9241
|
+
"- run_command is the general policy-gated command runner. Do not avoid it because a command might be disallowed; call run_command and let command policy return the allowed, rejected, or approval result.",
|
|
9242
|
+
"- Prefer dedicated tools for file reads, file writes, edits, Git inspection, and searches.",
|
|
9243
|
+
"- Use run_command only for commands allowed by project command policy. Prefer dedicated read, edit, Git, and validator tools when they fit.",
|
|
9244
|
+
"- Do not use run_command for installs, deploys, network commands, destructive commands, interactive commands, file reads, file writes, Git inspection, or validation when run_validator can do it."
|
|
9245
|
+
] : [],
|
|
8957
9246
|
...canUseTool("edit_file") && canUseTool("read_file") ? ["- Use read_file before editing a file so your edit is based on current file content and hash metadata."] : [],
|
|
8958
9247
|
...canUseTool("read_file") && (canUseTool("edit_file") || canUseTool("write_file")) ? ["- When passing expected_current_hash to edit_file or write_file, use the current pre-edit/pre-write hash from the latest read_file result for that exact file. Never invent it and never use a predicted after-edit or after-write hash."] : [],
|
|
8959
9248
|
...canUseTool("edit_file") ? ["- Use edit_file for targeted edits to existing files. Make multiple disjoint edits for the same file in one call when possible."] : [],
|
|
@@ -9578,6 +9867,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9578
9867
|
options;
|
|
9579
9868
|
taskPlan = createTaskPlanController();
|
|
9580
9869
|
approvedRunCommands = /* @__PURE__ */ new Set();
|
|
9870
|
+
startedHookSessionKeys = /* @__PURE__ */ new Set();
|
|
9581
9871
|
/**
|
|
9582
9872
|
* Holds the shared application context for one runtime instance.
|
|
9583
9873
|
* The runtime does not own those dependencies; it coordinates the
|
|
@@ -9610,6 +9900,20 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9610
9900
|
async checkKnowledgeBase() {
|
|
9611
9901
|
return getKnowledgeStatusEvents(await this.getKnowledgeStatusWithNonCleanFileCount());
|
|
9612
9902
|
}
|
|
9903
|
+
async runSessionStartHooks(session, options = {}) {
|
|
9904
|
+
const sessionKey = session?.sessionId ?? `workspace:${this.context.workspaceRoot}`;
|
|
9905
|
+
if (this.startedHookSessionKeys.has(sessionKey)) return [];
|
|
9906
|
+
this.startedHookSessionKeys.add(sessionKey);
|
|
9907
|
+
const result = await this.runHookEvent("SessionStart", this.createBaseHookPayload("SessionStart", session, {
|
|
9908
|
+
isResumed: Boolean(options.isResumed),
|
|
9909
|
+
taskStartAlias: "TaskStart"
|
|
9910
|
+
}), { abortSignal: options.abortSignal });
|
|
9911
|
+
return this.hookResultToEvents(result);
|
|
9912
|
+
}
|
|
9913
|
+
async runPreCompactHooks(session, options = {}) {
|
|
9914
|
+
const result = await this.runHookEvent("PreCompact", this.createBaseHookPayload("PreCompact", session, { reason: options.reason ?? "Compaction is about to start." }), { abortSignal: options.abortSignal });
|
|
9915
|
+
return this.hookResultToEvents(result);
|
|
9916
|
+
}
|
|
9613
9917
|
/**
|
|
9614
9918
|
* Streams one user chat turn through the agent loop. It builds the model
|
|
9615
9919
|
* prompt with relevant KB context, calls the model, executes any requested
|
|
@@ -9622,13 +9926,26 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9622
9926
|
* ordered events.
|
|
9623
9927
|
*/
|
|
9624
9928
|
async *submitMessageStream(conversation, message, abortSignal, options = {}) {
|
|
9625
|
-
|
|
9929
|
+
const session = options.session ?? this.options.session;
|
|
9930
|
+
for (const event of await this.runSessionStartHooks(session, { abortSignal })) yield event;
|
|
9931
|
+
const userPromptHook = await this.runHookEvent("UserPromptSubmit", this.createBaseHookPayload("UserPromptSubmit", session, {
|
|
9932
|
+
prompt: { text: message },
|
|
9933
|
+
prompt_text: message,
|
|
9934
|
+
user_prompt: message
|
|
9935
|
+
}), { abortSignal });
|
|
9936
|
+
for (const event of this.hookResultToEvents(userPromptHook)) yield event;
|
|
9937
|
+
if (userPromptHook.blocked || userPromptHook.stopped) {
|
|
9938
|
+
const interruption = userPromptHook.blocked ?? userPromptHook.stopped;
|
|
9939
|
+
if (userPromptHook.messages.length === 0) yield agentEvent.systemMessage(interruption.message);
|
|
9940
|
+
yield agentEvent.status("ready");
|
|
9941
|
+
return;
|
|
9942
|
+
}
|
|
9943
|
+
let nextPrompt = this.appendHookContextsToPrompt(await this.buildPromptWithKnowledgeContext(buildConversationPrompt(conversation, message), message), "UserPromptSubmit", userPromptHook.contexts);
|
|
9626
9944
|
let totalDurationMs = 0;
|
|
9627
9945
|
const tokenUsageTotals = {};
|
|
9628
9946
|
const profile = this.options.profile ?? PRIMARY_AGENT_PROFILE;
|
|
9629
9947
|
const permissions = createToolPermissionView(profile, { deniedTools: this.options.parentPermissions?.deniedTools });
|
|
9630
9948
|
const tools = getProfileToolDefinitions(permissions);
|
|
9631
|
-
const session = options.session ?? this.options.session;
|
|
9632
9949
|
const subagents = new SubagentManager({
|
|
9633
9950
|
context: this.context,
|
|
9634
9951
|
parentSession: session,
|
|
@@ -9733,7 +10050,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9733
10050
|
}
|
|
9734
10051
|
yield agentEvent.taskPlan(this.taskPlan.update({ items: [] }));
|
|
9735
10052
|
}
|
|
9736
|
-
|
|
10053
|
+
const finalMessage = finalText.trim() || "I got an empty response from the model.";
|
|
10054
|
+
yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
|
|
10055
|
+
for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
|
|
9737
10056
|
yield agentEvent.status("ready");
|
|
9738
10057
|
return;
|
|
9739
10058
|
}
|
|
@@ -9750,10 +10069,33 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9750
10069
|
if (modelToolCalls.length > 1 && modelToolCalls.every((call) => call.tool === "task")) {
|
|
9751
10070
|
const taskCalls = modelToolCalls.map((call) => call);
|
|
9752
10071
|
const taskResults = [];
|
|
10072
|
+
const postHookContexts = [];
|
|
9753
10073
|
for (let index = 0; index < taskCalls.length; index += DEFAULT_TASK_CONCURRENCY) {
|
|
9754
10074
|
const batch = taskCalls.slice(index, index + DEFAULT_TASK_CONCURRENCY);
|
|
10075
|
+
const executableBatch = [];
|
|
10076
|
+
for (let batchIndex = 0; batchIndex < batch.length; batchIndex += 1) {
|
|
10077
|
+
const call = batch[batchIndex];
|
|
10078
|
+
const resultIndex = index + batchIndex;
|
|
10079
|
+
const preHook = await this.runPreToolUseHook(call, modelToolCalls[resultIndex]?.id, session, abortSignal);
|
|
10080
|
+
for (const event of this.hookResultToEvents(preHook)) yield event;
|
|
10081
|
+
if (preHook.stopped) {
|
|
10082
|
+
if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
|
|
10083
|
+
yield agentEvent.status("ready");
|
|
10084
|
+
return;
|
|
10085
|
+
}
|
|
10086
|
+
if (preHook.blocked) {
|
|
10087
|
+
taskResults[resultIndex] = createToolErrorResult(call.tool, preHook.blocked.message);
|
|
10088
|
+
continue;
|
|
10089
|
+
}
|
|
10090
|
+
executableBatch.push({
|
|
10091
|
+
call,
|
|
10092
|
+
resultIndex,
|
|
10093
|
+
toolCallId: modelToolCalls[resultIndex]?.id
|
|
10094
|
+
});
|
|
10095
|
+
}
|
|
10096
|
+
if (executableBatch.length === 0) continue;
|
|
9755
10097
|
const taskEventQueue = createRuntimeEventQueue();
|
|
9756
|
-
const batchResultPromise = Promise.all(
|
|
10098
|
+
const batchResultPromise = Promise.all(executableBatch.map((entry) => executeToolCall(this.context.workspaceRoot, entry.call, {
|
|
9757
10099
|
logger: this.context.logger,
|
|
9758
10100
|
config: this.context.config,
|
|
9759
10101
|
taskPlan: this.taskPlan,
|
|
@@ -9761,22 +10103,60 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9761
10103
|
permissions,
|
|
9762
10104
|
subagents,
|
|
9763
10105
|
abortSignal,
|
|
9764
|
-
toolCallId:
|
|
10106
|
+
toolCallId: entry.toolCallId,
|
|
9765
10107
|
eventSink: (event) => taskEventQueue.push(event)
|
|
9766
10108
|
}))).finally(() => {
|
|
9767
10109
|
taskEventQueue.close();
|
|
9768
10110
|
});
|
|
9769
10111
|
for await (const event of taskEventQueue) yield event;
|
|
9770
|
-
|
|
10112
|
+
const batchResults = await batchResultPromise;
|
|
10113
|
+
for (let batchIndex = 0; batchIndex < executableBatch.length; batchIndex += 1) {
|
|
10114
|
+
const entry = executableBatch[batchIndex];
|
|
10115
|
+
taskResults[entry.resultIndex] = batchResults[batchIndex];
|
|
10116
|
+
}
|
|
10117
|
+
}
|
|
10118
|
+
for (let index = 0; index < taskCalls.length; index += 1) {
|
|
10119
|
+
const call = taskCalls[index];
|
|
10120
|
+
const toolResult = taskResults[index];
|
|
10121
|
+
yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
|
|
10122
|
+
const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
|
|
10123
|
+
for (const event of this.hookResultToEvents(postHook)) yield event;
|
|
10124
|
+
postHookContexts.push(...postHook.contexts);
|
|
10125
|
+
if (postHook.stopped) {
|
|
10126
|
+
if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
|
|
10127
|
+
yield agentEvent.status("ready");
|
|
10128
|
+
return;
|
|
10129
|
+
}
|
|
9771
10130
|
}
|
|
9772
|
-
for (let index = 0; index < taskCalls.length; index += 1) yield agentEvent.toolCall(taskCalls[index], formatToolCallMessage(taskCalls[index], taskResults[index]));
|
|
9773
10131
|
afterTool = "task";
|
|
9774
|
-
nextPrompt = `${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"))}
|
|
10132
|
+
nextPrompt = this.appendHookContextsToPrompt(`${nextPrompt}\n\n${taskResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, taskResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHookContexts);
|
|
9775
10133
|
continue;
|
|
9776
10134
|
}
|
|
9777
10135
|
if (modelToolCalls.length > 1 && modelToolCalls.every((call) => isParallelSafeToolName(call.tool))) {
|
|
9778
10136
|
const parallelCalls = modelToolCalls.map((call) => call);
|
|
9779
|
-
const parallelResults =
|
|
10137
|
+
const parallelResults = [];
|
|
10138
|
+
const executableCalls = [];
|
|
10139
|
+
const postHookContexts = [];
|
|
10140
|
+
for (let index = 0; index < parallelCalls.length; index += 1) {
|
|
10141
|
+
const call = parallelCalls[index];
|
|
10142
|
+
const preHook = await this.runPreToolUseHook(call, modelToolCalls[index]?.id, session, abortSignal);
|
|
10143
|
+
for (const event of this.hookResultToEvents(preHook)) yield event;
|
|
10144
|
+
if (preHook.stopped) {
|
|
10145
|
+
if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
|
|
10146
|
+
yield agentEvent.status("ready");
|
|
10147
|
+
return;
|
|
10148
|
+
}
|
|
10149
|
+
if (preHook.blocked) {
|
|
10150
|
+
parallelResults[index] = createToolErrorResult(call.tool, preHook.blocked.message);
|
|
10151
|
+
continue;
|
|
10152
|
+
}
|
|
10153
|
+
executableCalls.push({
|
|
10154
|
+
call,
|
|
10155
|
+
resultIndex: index,
|
|
10156
|
+
toolCallId: modelToolCalls[index]?.id
|
|
10157
|
+
});
|
|
10158
|
+
}
|
|
10159
|
+
const executedResults = await Promise.all(executableCalls.map((entry) => executeToolCall(this.context.workspaceRoot, entry.call, {
|
|
9780
10160
|
logger: this.context.logger,
|
|
9781
10161
|
config: this.context.config,
|
|
9782
10162
|
taskPlan: this.taskPlan,
|
|
@@ -9784,48 +10164,86 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9784
10164
|
permissions,
|
|
9785
10165
|
subagents,
|
|
9786
10166
|
abortSignal,
|
|
9787
|
-
toolCallId:
|
|
10167
|
+
toolCallId: entry.toolCallId
|
|
9788
10168
|
})));
|
|
9789
|
-
for (let index = 0; index <
|
|
10169
|
+
for (let index = 0; index < executableCalls.length; index += 1) {
|
|
10170
|
+
const entry = executableCalls[index];
|
|
10171
|
+
parallelResults[entry.resultIndex] = executedResults[index];
|
|
10172
|
+
}
|
|
10173
|
+
for (let index = 0; index < parallelCalls.length; index += 1) {
|
|
10174
|
+
const call = parallelCalls[index];
|
|
10175
|
+
const toolResult = parallelResults[index];
|
|
10176
|
+
yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
|
|
10177
|
+
const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
|
|
10178
|
+
for (const event of this.hookResultToEvents(postHook)) yield event;
|
|
10179
|
+
postHookContexts.push(...postHook.contexts);
|
|
10180
|
+
if (postHook.stopped) {
|
|
10181
|
+
if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
|
|
10182
|
+
yield agentEvent.status("ready");
|
|
10183
|
+
return;
|
|
10184
|
+
}
|
|
10185
|
+
}
|
|
9790
10186
|
afterTool = parallelCalls.at(-1)?.tool;
|
|
9791
|
-
nextPrompt = `${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"))}
|
|
10187
|
+
nextPrompt = this.appendHookContextsToPrompt(`${nextPrompt}\n\n${parallelResults.map((toolResult) => formatToolResultForPrompt(toolResult)).join("\n\n")}\n\n${formatContinuationInstruction(result.toolProtocol, parallelResults.at(-1), isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHookContexts);
|
|
9792
10188
|
continue;
|
|
9793
10189
|
}
|
|
9794
10190
|
const executableToolCall = toolCall;
|
|
9795
10191
|
const suppressiblePlanTodoAnswer = getSuppressiblePlanTodoAnswer(executableToolCall, result.text, this.taskPlan.get());
|
|
9796
10192
|
if (suppressiblePlanTodoAnswer !== void 0) {
|
|
9797
|
-
|
|
10193
|
+
const finalMessage = suppressiblePlanTodoAnswer || "I got an empty response from the model.";
|
|
10194
|
+
yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
|
|
10195
|
+
for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
|
|
9798
10196
|
yield agentEvent.status("ready");
|
|
9799
10197
|
return;
|
|
9800
10198
|
}
|
|
9801
|
-
const
|
|
10199
|
+
const preHook = await this.runPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
|
|
10200
|
+
for (const event of this.hookResultToEvents(preHook)) yield event;
|
|
9802
10201
|
let toolResult;
|
|
9803
|
-
if (
|
|
10202
|
+
if (preHook.stopped) {
|
|
10203
|
+
if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
|
|
10204
|
+
yield agentEvent.status("ready");
|
|
10205
|
+
return;
|
|
10206
|
+
}
|
|
10207
|
+
if (preHook.blocked) toolResult = createToolErrorResult(executableToolCall.tool, preHook.blocked.message);
|
|
9804
10208
|
else {
|
|
9805
|
-
const
|
|
9806
|
-
const
|
|
9807
|
-
|
|
9808
|
-
|
|
9809
|
-
|
|
9810
|
-
|
|
9811
|
-
|
|
9812
|
-
|
|
9813
|
-
|
|
9814
|
-
|
|
9815
|
-
|
|
9816
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
9821
|
-
|
|
10209
|
+
const approval = await this.resolveRunCommandApproval(executableToolCall, toolCall.id, options, session, abortSignal);
|
|
10210
|
+
for (const event of approval.events) yield event;
|
|
10211
|
+
if (approval.cancelled) toolResult = createToolErrorResult(executableToolCall.tool, approval.reason);
|
|
10212
|
+
else {
|
|
10213
|
+
const toolEventQueue = createRuntimeEventQueue();
|
|
10214
|
+
const toolResultPromise = executeToolCall(this.context.workspaceRoot, executableToolCall, {
|
|
10215
|
+
logger: this.context.logger,
|
|
10216
|
+
config: this.context.config,
|
|
10217
|
+
runCommandApprovals: { allowExactCommands: approval.approvedCommands },
|
|
10218
|
+
taskPlan: this.taskPlan,
|
|
10219
|
+
profile,
|
|
10220
|
+
permissions,
|
|
10221
|
+
subagents,
|
|
10222
|
+
abortSignal,
|
|
10223
|
+
toolCallId: toolCall.id,
|
|
10224
|
+
eventSink: (event) => toolEventQueue.push(event)
|
|
10225
|
+
}).finally(() => {
|
|
10226
|
+
toolEventQueue.close();
|
|
10227
|
+
});
|
|
10228
|
+
for await (const event of toolEventQueue) yield event;
|
|
10229
|
+
toolResult = await toolResultPromise;
|
|
10230
|
+
}
|
|
9822
10231
|
}
|
|
9823
10232
|
yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult));
|
|
9824
10233
|
if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") yield agentEvent.taskPlan(toolResult.plan);
|
|
10234
|
+
const postHook = await this.runPostToolUseHook(executableToolCall, toolCall.id, toolResult, session, abortSignal);
|
|
10235
|
+
for (const event of this.hookResultToEvents(postHook)) yield event;
|
|
10236
|
+
if (postHook.stopped) {
|
|
10237
|
+
if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
|
|
10238
|
+
yield agentEvent.status("ready");
|
|
10239
|
+
return;
|
|
10240
|
+
}
|
|
9825
10241
|
afterTool = executableToolCall.tool;
|
|
9826
|
-
nextPrompt = `${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}
|
|
10242
|
+
nextPrompt = this.appendHookContextsToPrompt(`${nextPrompt}\n\n${formatToolResultForPrompt(toolResult)}\n\n${formatContinuationInstruction(result.toolProtocol, toolResult, isToolAllowed(permissions, "plan_todo"))}`, "PostToolUse", postHook.contexts);
|
|
9827
10243
|
}
|
|
9828
|
-
|
|
10244
|
+
const finalMessage = "I stopped because the tool loop ended unexpectedly.";
|
|
10245
|
+
yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
|
|
10246
|
+
for (const event of await this.runStopHookEvents(session, finalMessage, "failed", abortSignal)) yield event;
|
|
9829
10247
|
yield agentEvent.status("ready");
|
|
9830
10248
|
}
|
|
9831
10249
|
/**
|
|
@@ -9840,16 +10258,80 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9840
10258
|
}
|
|
9841
10259
|
return events;
|
|
9842
10260
|
}
|
|
9843
|
-
async
|
|
10261
|
+
async runPreToolUseHook(call, toolCallId, session, abortSignal) {
|
|
10262
|
+
return this.runHookEvent("PreToolUse", this.createToolHookPayload("PreToolUse", call, toolCallId, session), {
|
|
10263
|
+
toolName: call.tool,
|
|
10264
|
+
abortSignal
|
|
10265
|
+
});
|
|
10266
|
+
}
|
|
10267
|
+
async runPostToolUseHook(call, toolCallId, result, session, abortSignal) {
|
|
10268
|
+
return this.runHookEvent("PostToolUse", this.createToolHookPayload("PostToolUse", call, toolCallId, session, { result }), {
|
|
10269
|
+
toolName: call.tool,
|
|
10270
|
+
abortSignal
|
|
10271
|
+
});
|
|
10272
|
+
}
|
|
10273
|
+
async runStopHookEvents(session, finalMessage, status, abortSignal) {
|
|
10274
|
+
const result = await this.runHookEvent("Stop", this.createBaseHookPayload("Stop", session, {
|
|
10275
|
+
taskCompleteAlias: "TaskComplete",
|
|
10276
|
+
finalMessage,
|
|
10277
|
+
status
|
|
10278
|
+
}), { abortSignal });
|
|
10279
|
+
return this.hookResultToEvents(result);
|
|
10280
|
+
}
|
|
10281
|
+
async runHookEvent(event, payload, options = {}) {
|
|
10282
|
+
return runTopchesterHooks(this.context, event, payload, options);
|
|
10283
|
+
}
|
|
10284
|
+
createBaseHookPayload(event, session, extra = {}) {
|
|
10285
|
+
return {
|
|
10286
|
+
hook_event_name: event,
|
|
10287
|
+
event,
|
|
10288
|
+
cwd: this.context.workspaceRoot,
|
|
10289
|
+
workspaceRoot: this.context.workspaceRoot,
|
|
10290
|
+
source: "topchester",
|
|
10291
|
+
...session ? {
|
|
10292
|
+
session_id: session.sessionId,
|
|
10293
|
+
sessionId: session.sessionId,
|
|
10294
|
+
session: {
|
|
10295
|
+
sessionId: session.sessionId,
|
|
10296
|
+
rootSessionId: session.metadata.rootSessionId,
|
|
10297
|
+
parentSessionId: session.metadata.parentSessionId,
|
|
10298
|
+
source: session.metadata.source
|
|
10299
|
+
}
|
|
10300
|
+
} : {},
|
|
10301
|
+
...extra
|
|
10302
|
+
};
|
|
10303
|
+
}
|
|
10304
|
+
createToolHookPayload(event, call, toolCallId, session, extra = {}) {
|
|
10305
|
+
return this.createBaseHookPayload(event, session, {
|
|
10306
|
+
tool_name: call.tool,
|
|
10307
|
+
tool_input: call.args,
|
|
10308
|
+
tool: {
|
|
10309
|
+
name: call.tool,
|
|
10310
|
+
input: call.args,
|
|
10311
|
+
...toolCallId ? { callId: toolCallId } : {}
|
|
10312
|
+
},
|
|
10313
|
+
...extra
|
|
10314
|
+
});
|
|
10315
|
+
}
|
|
10316
|
+
hookResultToEvents(result) {
|
|
10317
|
+
return result.messages.map((message) => agentEvent.systemMessage(message));
|
|
10318
|
+
}
|
|
10319
|
+
appendHookContextsToPrompt(prompt, event, contexts) {
|
|
10320
|
+
const hookContext = formatHookContextsForPrompt(event, contexts);
|
|
10321
|
+
return hookContext ? `${prompt}\n\n${hookContext}` : prompt;
|
|
10322
|
+
}
|
|
10323
|
+
async resolveRunCommandApproval(call, toolCallId, options, session, abortSignal) {
|
|
9844
10324
|
const approvedCommands = [...this.approvedRunCommands];
|
|
9845
10325
|
if (call.tool !== "run_command") return {
|
|
9846
10326
|
cancelled: false,
|
|
9847
|
-
approvedCommands
|
|
10327
|
+
approvedCommands,
|
|
10328
|
+
events: []
|
|
9848
10329
|
};
|
|
9849
10330
|
const parsed = runCommandArgsSchema.safeParse(call.args);
|
|
9850
10331
|
if (!parsed.success) return {
|
|
9851
10332
|
cancelled: false,
|
|
9852
|
-
approvedCommands
|
|
10333
|
+
approvedCommands,
|
|
10334
|
+
events: []
|
|
9853
10335
|
};
|
|
9854
10336
|
const decision = await validateRunCommandPolicy(parsed.data, {
|
|
9855
10337
|
workspaceRoot: this.context.workspaceRoot,
|
|
@@ -9858,13 +10340,31 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9858
10340
|
});
|
|
9859
10341
|
if (decision.allowed) return {
|
|
9860
10342
|
cancelled: false,
|
|
9861
|
-
approvedCommands
|
|
10343
|
+
approvedCommands,
|
|
10344
|
+
events: []
|
|
9862
10345
|
};
|
|
9863
10346
|
if (!isRunCommandApprovalEligible(decision.reason) || !options.requestRunCommandApproval) return {
|
|
9864
10347
|
cancelled: false,
|
|
9865
|
-
approvedCommands
|
|
10348
|
+
approvedCommands,
|
|
10349
|
+
events: []
|
|
9866
10350
|
};
|
|
9867
10351
|
const command = decision.commands[0] ?? parsed.data.command.trim();
|
|
10352
|
+
const actionRequiredHook = await this.runHookEvent("PermissionRequest", this.createToolHookPayload("PermissionRequest", call, toolCallId, session, {
|
|
10353
|
+
notification_type: "permission_prompt",
|
|
10354
|
+
permission_mode: "",
|
|
10355
|
+
command,
|
|
10356
|
+
workdir: parsed.data.workdir,
|
|
10357
|
+
reason: decision.reason
|
|
10358
|
+
}), {
|
|
10359
|
+
toolName: call.tool,
|
|
10360
|
+
abortSignal
|
|
10361
|
+
});
|
|
10362
|
+
const events = this.hookResultToEvents(actionRequiredHook);
|
|
10363
|
+
if (actionRequiredHook.blocked || actionRequiredHook.stopped) return {
|
|
10364
|
+
cancelled: true,
|
|
10365
|
+
reason: (actionRequiredHook.blocked ?? actionRequiredHook.stopped).message,
|
|
10366
|
+
events
|
|
10367
|
+
};
|
|
9868
10368
|
const approval = await options.requestRunCommandApproval({
|
|
9869
10369
|
command,
|
|
9870
10370
|
workdir: parsed.data.workdir,
|
|
@@ -9872,18 +10372,21 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
|
|
|
9872
10372
|
});
|
|
9873
10373
|
if (approval === "cancel") return {
|
|
9874
10374
|
cancelled: true,
|
|
9875
|
-
reason: `run_command cancelled by user for '${command}'
|
|
10375
|
+
reason: `run_command cancelled by user for '${command}'.`,
|
|
10376
|
+
events
|
|
9876
10377
|
};
|
|
9877
10378
|
if (approval === "allow_session" || approval === "allow_repo") {
|
|
9878
10379
|
this.approvedRunCommands.add(command);
|
|
9879
10380
|
return {
|
|
9880
10381
|
cancelled: false,
|
|
9881
|
-
approvedCommands: [...this.approvedRunCommands]
|
|
10382
|
+
approvedCommands: [...this.approvedRunCommands],
|
|
10383
|
+
events
|
|
9882
10384
|
};
|
|
9883
10385
|
}
|
|
9884
10386
|
return {
|
|
9885
10387
|
cancelled: false,
|
|
9886
|
-
approvedCommands: [...approvedCommands, command]
|
|
10388
|
+
approvedCommands: [...approvedCommands, command],
|
|
10389
|
+
events
|
|
9887
10390
|
};
|
|
9888
10391
|
}
|
|
9889
10392
|
/**
|
|
@@ -10368,6 +10871,7 @@ var TopchesterTuiShell = class {
|
|
|
10368
10871
|
const isResumed = this.options.session !== void 0;
|
|
10369
10872
|
const messages = this.options.initialMessages ?? getStartupThreadMessages(this.context);
|
|
10370
10873
|
if (!isResumed) await persistMessagesWithWarning(session, messages, messages);
|
|
10874
|
+
await this.appendStartupRuntimeEvents(session, messages, await this.runtime.runSessionStartHooks?.(session, { isResumed }) ?? []);
|
|
10371
10875
|
const folderName = getFolderName(this.context.workspaceRoot);
|
|
10372
10876
|
const modelLabel = getModelLabel(this.context);
|
|
10373
10877
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -10837,10 +11341,24 @@ var TopchesterTuiShell = class {
|
|
|
10837
11341
|
this.session = session;
|
|
10838
11342
|
this.sessionStartedAt = Date.now();
|
|
10839
11343
|
await persistMessagesWithWarning(session, messages, messages);
|
|
11344
|
+
await this.appendStartupRuntimeEvents(session, messages, await this.runtime.runSessionStartHooks?.(session, { isResumed: false }) ?? []);
|
|
10840
11345
|
app.resetForNewSession(messages);
|
|
10841
11346
|
tui.requestRender();
|
|
10842
11347
|
await this.checkAgent(app, tui);
|
|
10843
11348
|
}
|
|
11349
|
+
async appendStartupRuntimeEvents(session, messages, events) {
|
|
11350
|
+
for (const event of events) {
|
|
11351
|
+
messages.push(...renderRuntimeEvent(event));
|
|
11352
|
+
const payload = runtimeEventToSessionPayload(event);
|
|
11353
|
+
if (!payload) continue;
|
|
11354
|
+
try {
|
|
11355
|
+
await session.append(payload);
|
|
11356
|
+
} catch (error) {
|
|
11357
|
+
messages.push(systemMessage(`Session save failed: ${formatPlainError(error)}`));
|
|
11358
|
+
return;
|
|
11359
|
+
}
|
|
11360
|
+
}
|
|
11361
|
+
}
|
|
10844
11362
|
async applyRuntimeEvents(app, events, renderRequester) {
|
|
10845
11363
|
for (const event of events) {
|
|
10846
11364
|
if (event.type === "status") app.setStatus(event.status);
|
|
@@ -10929,6 +11447,16 @@ async function executeRunCommand(context, options) {
|
|
|
10929
11447
|
});
|
|
10930
11448
|
try {
|
|
10931
11449
|
if (!options.resume) await persistStartupMessages(session, runContext);
|
|
11450
|
+
await applyRuntimeEvents({
|
|
11451
|
+
events: await runtime.runSessionStartHooks(session, {
|
|
11452
|
+
isResumed: Boolean(options.resume),
|
|
11453
|
+
abortSignal: abortController.signal
|
|
11454
|
+
}),
|
|
11455
|
+
session,
|
|
11456
|
+
jsonEvents,
|
|
11457
|
+
runId,
|
|
11458
|
+
plain: !options.json
|
|
11459
|
+
});
|
|
10932
11460
|
await applyRuntimeEvents({
|
|
10933
11461
|
events: await runtime.checkKnowledgeBase(),
|
|
10934
11462
|
session,
|
|
@@ -11169,15 +11697,6 @@ program.command("dev").description("start local development mode").action(() =>
|
|
|
11169
11697
|
console.log("Topchester local dev mode");
|
|
11170
11698
|
printStartupSummary(context);
|
|
11171
11699
|
});
|
|
11172
|
-
program.command("update").alias("upgrade").description("update Topchester with the package manager that installed it").argument("[target]", "version or npm dist tag to install", "latest").action(async (target) => {
|
|
11173
|
-
try {
|
|
11174
|
-
const command = await runSelfUpdate({ target });
|
|
11175
|
-
console.log(formatSelfUpdateSuccess(command).join("\n"));
|
|
11176
|
-
} catch (error) {
|
|
11177
|
-
console.error(formatStartupError(error));
|
|
11178
|
-
process.exitCode = 1;
|
|
11179
|
-
}
|
|
11180
|
-
});
|
|
11181
11700
|
program.command("run").description("run one prompt or slash command without opening the TUI").argument("<prompt...>", "prompt text or slash command").option("--model <model>", "override the agent.primary model for this run").option("--timeout <ms>", "timeout for the run in milliseconds", parsePositiveInteger).option("--json", "write JSONL run events to stdout").option("--output-json <path>", "write JSONL run events to a file").action(async (promptParts, options) => {
|
|
11182
11701
|
const context = createContextFromOptions();
|
|
11183
11702
|
const globalOptions = program.opts();
|
|
@@ -11237,6 +11756,15 @@ kbCommand.command("status").description("show project files that are not current
|
|
|
11237
11756
|
const result = await ui.spinner("Checking KB file status...", async () => filterNonCleanKnowledgeCompileResult(await dryRunKnowledgeCompile(context.workspaceRoot, { config: context.config })));
|
|
11238
11757
|
console.log(formatKnowledgeCompileStatusResult(result, { formatSyncStatus: formatDryRunSyncStatus }).join("\n"));
|
|
11239
11758
|
});
|
|
11759
|
+
program.command("update").alias("upgrade").description("update Topchester with the package manager that installed it").argument("[target]", "version or npm dist tag to install", "latest").action(async (target) => {
|
|
11760
|
+
try {
|
|
11761
|
+
const command = await runSelfUpdate({ target });
|
|
11762
|
+
console.log(formatSelfUpdateSuccess(command).join("\n"));
|
|
11763
|
+
} catch (error) {
|
|
11764
|
+
console.error(formatStartupError(error));
|
|
11765
|
+
process.exitCode = 1;
|
|
11766
|
+
}
|
|
11767
|
+
});
|
|
11240
11768
|
await program.parseAsync();
|
|
11241
11769
|
function printStartupSummary(context) {
|
|
11242
11770
|
const assignments = context.config.models?.assignments ?? {};
|