topchester-ai 0.23.0 → 0.24.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 CHANGED
@@ -5273,6 +5273,7 @@ const hookHandlerSchema = z.object({
5273
5273
  type: z.literal("command").optional(),
5274
5274
  command: z.string().min(1),
5275
5275
  timeoutMs: hookTimeoutMsSchema.optional(),
5276
+ statusMessage: z.string().min(1).optional(),
5276
5277
  matcher: hookMatcherSchema
5277
5278
  }).strict();
5278
5279
  const canonicalHooksConfigSchema = z.object({
@@ -7567,6 +7568,14 @@ function toolCallMessage(call, label, resultSummary) {
7567
7568
  resultSummary
7568
7569
  };
7569
7570
  }
7571
+ function hookStatusMessage(label, eventName, statusMessage) {
7572
+ return {
7573
+ kind: "hook_status",
7574
+ label,
7575
+ ...eventName === void 0 ? {} : { eventName },
7576
+ ...statusMessage === void 0 ? {} : { statusMessage }
7577
+ };
7578
+ }
7570
7579
  function subagentMessage(message) {
7571
7580
  return {
7572
7581
  kind: "subagent",
@@ -7583,6 +7592,7 @@ const DEFAULT_MODAL_VISIBLE_ACTION_LIMIT = 16;
7583
7592
  function renderChatMessage(message, options = {}) {
7584
7593
  if (message.kind === "modal") return renderChatModal(message, options.selectedActionIndex, options.maxModalHeight);
7585
7594
  if (message.kind === "tool_call") return renderToolCallMessage(message);
7595
+ if (message.kind === "hook_status") return renderHookStatusMessage(message);
7586
7596
  if (message.kind === "subagent") return renderSubagentMessage(message);
7587
7597
  if (message.kind === "thinking") return message.text.split("\n").map((line) => ui.muted(line));
7588
7598
  if (message.text.length === 0) return [""];
@@ -7617,6 +7627,9 @@ function renderToolCallMessage(message) {
7617
7627
  const visibleLabel = message.resultSummary && !message.label.includes(message.resultSummary) ? `${message.label} ${message.resultSummary}` : message.label;
7618
7628
  return [` ${ui.muted(expandTabs(visibleLabel))}`];
7619
7629
  }
7630
+ function renderHookStatusMessage(message) {
7631
+ return [` ${ui.muted(expandTabs(message.label))}`];
7632
+ }
7620
7633
  function renderSubagentMessage(message) {
7621
7634
  const label = message.title ?? shortSessionId(message.sessionId);
7622
7635
  switch (message.status) {
@@ -7774,6 +7787,12 @@ const toolCallPayloadSchema = z.object({
7774
7787
  label: z.string(),
7775
7788
  call: z.record(z.string(), jsonValueSchema)
7776
7789
  });
7790
+ const hookStatusPayloadSchema = z.object({
7791
+ kind: z.literal("hook_status"),
7792
+ eventName: z.string(),
7793
+ statusMessage: z.string(),
7794
+ label: z.string()
7795
+ });
7777
7796
  const taskPlanItemPayloadSchema = z.object({
7778
7797
  text: z.string(),
7779
7798
  status: z.enum([
@@ -7840,6 +7859,7 @@ const subagentFailedPayloadSchema = subagentLifecycleBasePayloadSchema.extend({
7840
7859
  const sessionEventPayloadSchema = z.discriminatedUnion("kind", [
7841
7860
  messagePayloadSchema,
7842
7861
  toolCallPayloadSchema,
7862
+ hookStatusPayloadSchema,
7843
7863
  taskPlanPayloadSchema,
7844
7864
  instructionContextPayloadSchema,
7845
7865
  statusPayloadSchema,
@@ -8012,6 +8032,9 @@ function rehydrateSession(events) {
8012
8032
  case "tool_call":
8013
8033
  messages.push(toolCallMessage(event.call, event.label));
8014
8034
  break;
8035
+ case "hook_status":
8036
+ messages.push(hookStatusMessage(event.label, event.eventName, event.statusMessage));
8037
+ break;
8015
8038
  case "task_plan":
8016
8039
  taskPlan = {
8017
8040
  items: event.items,
@@ -8638,6 +8661,14 @@ const agentEvent = {
8638
8661
  label
8639
8662
  };
8640
8663
  },
8664
+ hookStatus(eventName, statusMessage) {
8665
+ return {
8666
+ type: "hook_status",
8667
+ eventName,
8668
+ statusMessage,
8669
+ label: `🪝 hook>${formatHookEventName(eventName)}: ${statusMessage}`
8670
+ };
8671
+ },
8641
8672
  taskPlan(plan) {
8642
8673
  return {
8643
8674
  type: "task_plan",
@@ -8698,6 +8729,9 @@ function choiceAction(label, value) {
8698
8729
  value
8699
8730
  };
8700
8731
  }
8732
+ function formatHookEventName(eventName) {
8733
+ return eventName.replace(/([a-z])([A-Z])/gu, "$1-$2").toLowerCase();
8734
+ }
8701
8735
  //#endregion
8702
8736
  //#region src/tui/keys.ts
8703
8737
  function isUpKey(data) {
@@ -9066,6 +9100,7 @@ var ChatLayout = class {
9066
9100
  case "system":
9067
9101
  case "thinking":
9068
9102
  case "tool_call":
9103
+ case "hook_status":
9069
9104
  case "subagent":
9070
9105
  case "modal": return [];
9071
9106
  }
@@ -9638,6 +9673,11 @@ async function runTopchesterHooks(context, event, payload, options = {}) {
9638
9673
  };
9639
9674
  for (const handler of handlers) {
9640
9675
  result.handlerCount += 1;
9676
+ const statusMessage = handler.statusMessage?.trim();
9677
+ if (statusMessage) options.onHookStart?.({
9678
+ event,
9679
+ statusMessage
9680
+ });
9641
9681
  const handlerResult = await runCommandHandler(context, event, payload, handler, options);
9642
9682
  result.contexts.push(...handlerResult.contexts);
9643
9683
  result.messages.push(...handlerResult.messages);
@@ -10210,6 +10250,12 @@ function runtimeEventToSessionPayload(event) {
10210
10250
  label: event.label,
10211
10251
  call: event.call
10212
10252
  };
10253
+ case "hook_status": return {
10254
+ kind: "hook_status",
10255
+ eventName: event.eventName,
10256
+ statusMessage: event.statusMessage,
10257
+ label: event.label
10258
+ };
10213
10259
  case "task_plan": return {
10214
10260
  kind: "task_plan",
10215
10261
  items: event.plan.items,
@@ -10851,15 +10897,27 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
10851
10897
  const sessionKey = session?.sessionId ?? `workspace:${this.context.workspaceRoot}`;
10852
10898
  if (this.startedHookSessionKeys.has(sessionKey)) return [];
10853
10899
  this.startedHookSessionKeys.add(sessionKey);
10900
+ const startedEvents = [];
10854
10901
  const result = await this.runHookEvent("SessionStart", this.createBaseHookPayload("SessionStart", session, {
10855
10902
  isResumed: Boolean(options.isResumed),
10856
10903
  taskStartAlias: "TaskStart"
10857
- }), { abortSignal: options.abortSignal });
10858
- return this.hookResultToEvents(result);
10904
+ }), {
10905
+ abortSignal: options.abortSignal,
10906
+ onHookStart: (status) => {
10907
+ startedEvents.push(agentEvent.hookStatus(status.event, status.statusMessage));
10908
+ }
10909
+ });
10910
+ return [...startedEvents, ...this.hookResultToEvents(result)];
10859
10911
  }
10860
10912
  async runPreCompactHooks(session, options = {}) {
10861
- const result = await this.runHookEvent("PreCompact", this.createBaseHookPayload("PreCompact", session, { reason: options.reason ?? "Compaction is about to start." }), { abortSignal: options.abortSignal });
10862
- return this.hookResultToEvents(result);
10913
+ const startedEvents = [];
10914
+ const result = await this.runHookEvent("PreCompact", this.createBaseHookPayload("PreCompact", session, { reason: options.reason ?? "Compaction is about to start." }), {
10915
+ abortSignal: options.abortSignal,
10916
+ onHookStart: (status) => {
10917
+ startedEvents.push(agentEvent.hookStatus(status.event, status.statusMessage));
10918
+ }
10919
+ });
10920
+ return [...startedEvents, ...this.hookResultToEvents(result)];
10863
10921
  }
10864
10922
  /**
10865
10923
  * Streams one user chat turn through the agent loop. It builds the model
@@ -10875,11 +10933,13 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
10875
10933
  async *submitMessageStream(conversation, message, abortSignal, options = {}) {
10876
10934
  const session = options.session ?? this.options.session;
10877
10935
  for (const event of await this.runSessionStartHooks(session, { abortSignal })) yield event;
10878
- const userPromptHook = await this.runHookEvent("UserPromptSubmit", this.createBaseHookPayload("UserPromptSubmit", session, {
10936
+ const userPromptHookRun = this.startHookEvent("UserPromptSubmit", this.createBaseHookPayload("UserPromptSubmit", session, {
10879
10937
  prompt: { text: message },
10880
10938
  prompt_text: message,
10881
10939
  user_prompt: message
10882
10940
  }), { abortSignal });
10941
+ for await (const event of userPromptHookRun.statusEvents) yield event;
10942
+ const userPromptHook = await userPromptHookRun.result;
10883
10943
  for (const event of this.hookResultToEvents(userPromptHook)) yield event;
10884
10944
  if (userPromptHook.blocked || userPromptHook.stopped) {
10885
10945
  const interruption = userPromptHook.blocked ?? userPromptHook.stopped;
@@ -11008,7 +11068,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11008
11068
  }
11009
11069
  const finalMessage = finalText.trim() || "I got an empty response from the model.";
11010
11070
  yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
11011
- for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
11071
+ for await (const event of this.streamStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
11012
11072
  yield agentEvent.status("ready");
11013
11073
  return;
11014
11074
  }
@@ -11032,7 +11092,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11032
11092
  for (let batchIndex = 0; batchIndex < batch.length; batchIndex += 1) {
11033
11093
  const call = batch[batchIndex];
11034
11094
  const resultIndex = index + batchIndex;
11035
- const preHook = await this.runPreToolUseHook(call, modelToolCalls[resultIndex]?.id, session, abortSignal);
11095
+ const preHookRun = this.startPreToolUseHook(call, modelToolCalls[resultIndex]?.id, session, abortSignal);
11096
+ for await (const event of preHookRun.statusEvents) yield event;
11097
+ const preHook = await preHookRun.result;
11036
11098
  for (const event of this.hookResultToEvents(preHook)) yield event;
11037
11099
  if (preHook.stopped) {
11038
11100
  if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
@@ -11078,7 +11140,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11078
11140
  const toolResult = taskResults[index];
11079
11141
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
11080
11142
  yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
11081
- const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11143
+ const postHookRun = this.startPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11144
+ for await (const event of postHookRun.statusEvents) yield event;
11145
+ const postHook = await postHookRun.result;
11082
11146
  for (const event of this.hookResultToEvents(postHook)) yield event;
11083
11147
  postHookContexts.push(...postHook.contexts);
11084
11148
  if (postHook.stopped) {
@@ -11098,7 +11162,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11098
11162
  const postHookContexts = [];
11099
11163
  for (let index = 0; index < parallelCalls.length; index += 1) {
11100
11164
  const call = parallelCalls[index];
11101
- const preHook = await this.runPreToolUseHook(call, modelToolCalls[index]?.id, session, abortSignal);
11165
+ const preHookRun = this.startPreToolUseHook(call, modelToolCalls[index]?.id, session, abortSignal);
11166
+ for await (const event of preHookRun.statusEvents) yield event;
11167
+ const preHook = await preHookRun.result;
11102
11168
  for (const event of this.hookResultToEvents(preHook)) yield event;
11103
11169
  if (preHook.stopped) {
11104
11170
  if (preHook.messages.length === 0) yield agentEvent.systemMessage(preHook.stopped.message);
@@ -11136,7 +11202,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11136
11202
  const toolResult = parallelResults[index];
11137
11203
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
11138
11204
  yield agentEvent.toolCall(call, formatToolCallMessage(call, toolResult));
11139
- const postHook = await this.runPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11205
+ const postHookRun = this.startPostToolUseHook(call, modelToolCalls[index]?.id, toolResult, session, abortSignal);
11206
+ for await (const event of postHookRun.statusEvents) yield event;
11207
+ const postHook = await postHookRun.result;
11140
11208
  for (const event of this.hookResultToEvents(postHook)) yield event;
11141
11209
  postHookContexts.push(...postHook.contexts);
11142
11210
  if (postHook.stopped) {
@@ -11154,11 +11222,13 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11154
11222
  if (suppressiblePlanTodoAnswer !== void 0) {
11155
11223
  const finalMessage = suppressiblePlanTodoAnswer || "I got an empty response from the model.";
11156
11224
  yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(result.modelId, totalDurationMs, tokenUsageTotals));
11157
- for (const event of await this.runStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
11225
+ for await (const event of this.streamStopHookEvents(session, finalMessage, "completed", abortSignal)) yield event;
11158
11226
  yield agentEvent.status("ready");
11159
11227
  return;
11160
11228
  }
11161
- const preHook = await this.runPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
11229
+ const preHookRun = this.startPreToolUseHook(executableToolCall, toolCall.id, session, abortSignal);
11230
+ for await (const event of preHookRun.statusEvents) yield event;
11231
+ const preHook = await preHookRun.result;
11162
11232
  for (const event of this.hookResultToEvents(preHook)) yield event;
11163
11233
  let toolResult;
11164
11234
  if (preHook.stopped) {
@@ -11196,7 +11266,9 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11196
11266
  for (const event of createInstructionContextEventsFromToolResult(toolResult, persistedProjectInstructionKeys)) yield event;
11197
11267
  yield agentEvent.toolCall(executableToolCall, formatToolCallMessage(executableToolCall, toolResult));
11198
11268
  if (!isToolErrorResult(toolResult) && toolResult.tool === "plan_todo") yield agentEvent.taskPlan(toolResult.plan);
11199
- const postHook = await this.runPostToolUseHook(executableToolCall, toolCall.id, toolResult, session, abortSignal);
11269
+ const postHookRun = this.startPostToolUseHook(executableToolCall, toolCall.id, toolResult, session, abortSignal);
11270
+ for await (const event of postHookRun.statusEvents) yield event;
11271
+ const postHook = await postHookRun.result;
11200
11272
  for (const event of this.hookResultToEvents(postHook)) yield event;
11201
11273
  if (postHook.stopped) {
11202
11274
  if (postHook.messages.length === 0) yield agentEvent.systemMessage(postHook.stopped.message);
@@ -11208,7 +11280,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11208
11280
  }
11209
11281
  const finalMessage = "I stopped because the tool loop ended unexpectedly.";
11210
11282
  yield agentEvent.assistantMessage(finalMessage, formatAgentMessageMeta(lastModelId, totalDurationMs, tokenUsageTotals));
11211
- for (const event of await this.runStopHookEvents(session, finalMessage, "failed", abortSignal)) yield event;
11283
+ for await (const event of this.streamStopHookEvents(session, finalMessage, "failed", abortSignal)) yield event;
11212
11284
  yield agentEvent.status("ready");
11213
11285
  }
11214
11286
  /**
@@ -11223,29 +11295,45 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11223
11295
  }
11224
11296
  return events;
11225
11297
  }
11226
- async runPreToolUseHook(call, toolCallId, session, abortSignal) {
11227
- return this.runHookEvent("PreToolUse", this.createToolHookPayload("PreToolUse", call, toolCallId, session), {
11298
+ startPreToolUseHook(call, toolCallId, session, abortSignal) {
11299
+ return this.startHookEvent("PreToolUse", this.createToolHookPayload("PreToolUse", call, toolCallId, session), {
11228
11300
  toolName: call.tool,
11229
11301
  abortSignal
11230
11302
  });
11231
11303
  }
11232
- async runPostToolUseHook(call, toolCallId, result, session, abortSignal) {
11233
- return this.runHookEvent("PostToolUse", this.createToolHookPayload("PostToolUse", call, toolCallId, session, { result }), {
11304
+ startPostToolUseHook(call, toolCallId, result, session, abortSignal) {
11305
+ return this.startHookEvent("PostToolUse", this.createToolHookPayload("PostToolUse", call, toolCallId, session, { result }), {
11234
11306
  toolName: call.tool,
11235
11307
  abortSignal
11236
11308
  });
11237
11309
  }
11238
- async runStopHookEvents(session, finalMessage, status, abortSignal) {
11239
- const result = await this.runHookEvent("Stop", this.createBaseHookPayload("Stop", session, {
11310
+ async *streamStopHookEvents(session, finalMessage, status, abortSignal) {
11311
+ const hookRun = this.startHookEvent("Stop", this.createBaseHookPayload("Stop", session, {
11240
11312
  taskCompleteAlias: "TaskComplete",
11241
11313
  finalMessage,
11242
11314
  status
11243
11315
  }), { abortSignal });
11244
- return this.hookResultToEvents(result);
11316
+ for await (const event of hookRun.statusEvents) yield event;
11317
+ const result = await hookRun.result;
11318
+ for (const event of this.hookResultToEvents(result)) yield event;
11245
11319
  }
11246
11320
  async runHookEvent(event, payload, options = {}) {
11247
11321
  return runTopchesterHooks(this.context, event, payload, options);
11248
11322
  }
11323
+ startHookEvent(event, payload, options = {}) {
11324
+ const queue = createRuntimeEventQueue();
11325
+ return {
11326
+ statusEvents: queue,
11327
+ result: this.runHookEvent(event, payload, {
11328
+ ...options,
11329
+ onHookStart: (status) => {
11330
+ queue.push(agentEvent.hookStatus(status.event, status.statusMessage));
11331
+ }
11332
+ }).finally(() => {
11333
+ queue.close();
11334
+ })
11335
+ };
11336
+ }
11249
11337
  createBaseHookPayload(event, session, extra = {}) {
11250
11338
  return {
11251
11339
  hook_event_name: event,
@@ -11253,6 +11341,7 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11253
11341
  cwd: this.context.workspaceRoot,
11254
11342
  workspaceRoot: this.context.workspaceRoot,
11255
11343
  source: "topchester",
11344
+ ...this.createHookModelPayload("agent.primary"),
11256
11345
  ...session ? {
11257
11346
  session_id: session.sessionId,
11258
11347
  sessionId: session.sessionId,
@@ -11266,6 +11355,33 @@ var TopchesterAgentRuntime = class TopchesterAgentRuntime {
11266
11355
  ...extra
11267
11356
  };
11268
11357
  }
11358
+ createHookModelPayload(purpose) {
11359
+ const resolveModel = this.context.modelGateway.resolveModel;
11360
+ if (typeof resolveModel !== "function") return {};
11361
+ try {
11362
+ const resolved = resolveModel.call(this.context.modelGateway, purpose);
11363
+ const modelRef = `${resolved.providerId}/${resolved.modelId}`;
11364
+ return {
11365
+ model_purpose: resolved.purpose,
11366
+ model_provider: resolved.providerId,
11367
+ model_id: resolved.modelId,
11368
+ model_ref: modelRef,
11369
+ model: {
11370
+ purpose: resolved.purpose,
11371
+ providerId: resolved.providerId,
11372
+ modelId: resolved.modelId,
11373
+ ref: modelRef
11374
+ }
11375
+ };
11376
+ } catch (error) {
11377
+ this.context.logger.debug({
11378
+ event: "hook_model_resolution_skipped",
11379
+ purpose,
11380
+ error: error instanceof Error ? error.message : String(error)
11381
+ }, "hook model metadata unavailable");
11382
+ return {};
11383
+ }
11384
+ }
11269
11385
  createToolHookPayload(event, call, toolCallId, session, extra = {}) {
11270
11386
  return this.createBaseHookPayload(event, session, {
11271
11387
  tool_name: call.tool,
@@ -11494,6 +11610,7 @@ function renderRuntimeEvent(event) {
11494
11610
  switch (event.type) {
11495
11611
  case "message": return [event.role === "assistant" ? agentMessage(event.text, event.meta) : systemMessage(event.text)];
11496
11612
  case "tool_call": return [toolCallMessage(event.call, event.label)];
11613
+ case "hook_status": return [hookStatusMessage(event.label, event.eventName, event.statusMessage)];
11497
11614
  case "knowledge_status": return [systemMessage([`KB status: ${formatKnowledgePathStatus(event.status)}${formatKbPathSource(event.status)}`, event.guidance].filter(Boolean).join("\n"))];
11498
11615
  case "choice": return [modalMessage({
11499
11616
  tone: event.tone,
@@ -11533,6 +11650,11 @@ function formatForwardedSubagentEvent(sessionId, event) {
11533
11650
  sessionId,
11534
11651
  text: event.label
11535
11652
  })];
11653
+ if (event.type === "hook_status") return [subagentMessage({
11654
+ status: "event",
11655
+ sessionId,
11656
+ text: event.label
11657
+ })];
11536
11658
  return [];
11537
11659
  }
11538
11660
  function formatKbPathSource(status) {
@@ -11731,6 +11853,12 @@ function chatMessageToSessionPayload(message) {
11731
11853
  };
11732
11854
  if (message.kind === "thinking") return;
11733
11855
  if (message.kind === "subagent") return;
11856
+ if (message.kind === "hook_status") return message.eventName && message.statusMessage ? {
11857
+ kind: "hook_status",
11858
+ eventName: message.eventName,
11859
+ statusMessage: message.statusMessage,
11860
+ label: message.label
11861
+ } : void 0;
11734
11862
  if (message.kind === "modal") return {
11735
11863
  kind: "choice",
11736
11864
  tone: message.tone,
@@ -12802,6 +12930,7 @@ async function loadConversation(workspaceRoot, resume) {
12802
12930
  case "system":
12803
12931
  case "thinking":
12804
12932
  case "tool_call":
12933
+ case "hook_status":
12805
12934
  case "subagent":
12806
12935
  case "modal": return [];
12807
12936
  }
@@ -12834,6 +12963,10 @@ function printPlainEvent(event) {
12834
12963
  console.log(event.label);
12835
12964
  return;
12836
12965
  }
12966
+ if (event.type === "hook_status") {
12967
+ console.log(event.label);
12968
+ return;
12969
+ }
12837
12970
  if (event.type === "knowledge_status" && event.guidance) {
12838
12971
  console.log(event.guidance);
12839
12972
  return;