@dimi-agent/cli 0.6.1 → 0.6.3

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.
Files changed (2) hide show
  1. package/dist/main.mjs +232 -93
  2. package/package.json +3 -3
package/dist/main.mjs CHANGED
@@ -224811,17 +224811,22 @@ var init_sdk_rpc_client = __esmMin((() => {
224811
224811
  }
224812
224812
  /**
224813
224813
  * Build one canonical v2 resume snapshot from the live restored scopes.
224814
- * Replay is the restored context history; task and todo state come from
224815
- * their owning services instead of a second pass over the wire journal.
224814
+ * Replay is the full message history folded from the agent's wire journal —
224815
+ * NOT the live context memory, which after a compaction collapses into
224816
+ * `[...keptUserMessages, compaction_summary]` and would lose the compacted
224817
+ * prefix. `context` keeps the folded (live) context for token accounting;
224818
+ * task and todo state come from their owning services instead of a second
224819
+ * pass over the wire journal.
224816
224820
  */
224817
224821
  async resumedAgentState(session, agent, type, replayTurnLimit) {
224818
224822
  const facade = this.klient.session(session.id).agent(agent.id);
224819
224823
  const ctx = session.accessor.get(ISessionContext);
224820
- const [context, plan, usage, tasks] = await Promise.all([
224824
+ const [context, plan, usage, tasks, replay] = await Promise.all([
224821
224825
  facade.getContext(),
224822
224826
  facade.getPlan(),
224823
224827
  facade.getUsage(),
224824
- facade.getTasks({ activeOnly: false })
224828
+ facade.getTasks({ activeOnly: false }),
224829
+ this.buildReplayFromWire(agent)
224825
224830
  ]);
224826
224831
  const profile = agent.accessor.get(IAgentProfileService).data();
224827
224832
  return {
@@ -224835,11 +224840,7 @@ var init_sdk_rpc_client = __esmMin((() => {
224835
224840
  systemPrompt: profile.systemPrompt
224836
224841
  },
224837
224842
  context,
224838
- replay: limitAgentReplayByTurns(context.history.map((message, time) => ({
224839
- type: "message",
224840
- message,
224841
- time
224842
- })), replayTurnLimit),
224843
+ replay: limitAgentReplayByTurns(replay, replayTurnLimit),
224843
224844
  permission: {
224844
224845
  mode: agent.accessor.get(IAgentPermissionModeService).mode,
224845
224846
  rules: [...agent.accessor.get(IAgentPermissionRulesService).rules]
@@ -224853,6 +224854,63 @@ var init_sdk_rpc_client = __esmMin((() => {
224853
224854
  };
224854
224855
  }
224855
224856
  /**
224857
+ * Fold the agent's full message history from its wire journal.
224858
+ *
224859
+ * The wire journal is the source of truth for message history: it keeps
224860
+ * every `context.append_message` / `context.append_loop_event` record, and
224861
+ * a `context.apply_compaction` record only marks a compaction point instead
224862
+ * of rewriting the past. Folding it yields the complete pre-compaction
224863
+ * history with a summary marker at each compaction, which is what replay
224864
+ * should render. The live context memory (`facade.getContext()`) is the
224865
+ * model's folded context and is deliberately NOT used here — it would hide
224866
+ * everything compacted away.
224867
+ */
224868
+ async buildReplayFromWire(agent) {
224869
+ await agent.accessor.get(IWireService).flush();
224870
+ const scope = agent.accessor.get(IAgentScopeContext).scope();
224871
+ const reducer = createContextTranscriptReducer();
224872
+ const applyCompactionRecords = [];
224873
+ for await (const record of this.engineAccessor.get(IAppendLogStore).read(scope, AGENT_WIRE_RECORD_KEY)) {
224874
+ if (record.type === "context.apply_compaction") applyCompactionRecords.push(record);
224875
+ reducer.add(record);
224876
+ }
224877
+ const { entries, times } = reducer.result();
224878
+ const replay = [];
224879
+ let compactionIndex = 0;
224880
+ for (let i = 0; i < entries.length; i++) {
224881
+ const message = entries[i];
224882
+ const time = times[i];
224883
+ if (message.origin?.kind === "compaction_summary") {
224884
+ const record = applyCompactionRecords[compactionIndex];
224885
+ compactionIndex += 1;
224886
+ if (record !== void 0) {
224887
+ const compaction = readContextCompactionRecord(record);
224888
+ replay.push({
224889
+ type: "compaction",
224890
+ result: {
224891
+ summary: compaction.summary,
224892
+ contextSummary: compaction.contextSummary,
224893
+ compactedCount: compaction.compactedCount,
224894
+ tokensBefore: compaction.tokensBefore,
224895
+ tokensAfter: compaction.tokensAfter,
224896
+ keptUserMessageCount: compaction.keptUserMessageCount,
224897
+ keptHeadUserMessageCount: compaction.keptHeadUserMessageCount,
224898
+ droppedCount: compaction.droppedCount
224899
+ },
224900
+ time: time ?? 0
224901
+ });
224902
+ continue;
224903
+ }
224904
+ }
224905
+ replay.push({
224906
+ type: "message",
224907
+ message,
224908
+ time: time ?? 0
224909
+ });
224910
+ }
224911
+ return replay;
224912
+ }
224913
+ /**
224856
224914
  * Every v2 workspace-id bucket addressing `workDir` (already normalized):
224857
224915
  * the registered workspace's alias set when the catalog knows the root, or
224858
224916
  * the freshly minted bucket key for index-only sessions (mirrors how v1's
@@ -378194,6 +378252,21 @@ var init_tool_call = __esmMin((() => {
378194
378252
  get toolCallView() {
378195
378253
  return this.toolCall;
378196
378254
  }
378255
+ /**
378256
+ * Whether this call has reached a terminal state. Ordinary tools finish when
378257
+ * their result lands; agent and read cards derive their phase from events
378258
+ * (subagent lifecycle / read status), falling back to the result. Used by
378259
+ * transcript folding to avoid collapsing calls the user still needs to
378260
+ * watch (a running agent would otherwise disappear into the summary line).
378261
+ */
378262
+ isFinished() {
378263
+ if (this.toolCall.name === "Agent" || this.toolCall.name === "AgentSwarm") {
378264
+ const phase = this.getSubagentSnapshot().phase;
378265
+ return phase === "done" || phase === "failed" || phase === "backgrounded";
378266
+ }
378267
+ if (this.toolCall.name === "Read") return this.getReadSnapshot().phase !== "pending";
378268
+ return this.result !== void 0;
378269
+ }
378197
378270
  /** Notifies the listener when internal state changes, if a group is attached. */
378198
378271
  notifySnapshotChange() {
378199
378272
  this.onSnapshotChange?.();
@@ -386868,6 +386941,10 @@ var init_session_replay = __esmMin((() => {
386868
386941
  return;
386869
386942
  }
386870
386943
  if (message.origin?.kind === "injection") return;
386944
+ if (message.origin?.kind === "compaction_summary") {
386945
+ this.renderCompactionSummary(context, message);
386946
+ return;
386947
+ }
386871
386948
  if (message.origin?.kind === "task") {
386872
386949
  this.flushAssistant(context);
386873
386950
  const info = this.host.sessionEventHandler.backgroundTasks.get(message.origin.taskId);
@@ -387036,6 +387113,21 @@ var init_session_replay = __esmMin((() => {
387036
387113
  }
387037
387114
  });
387038
387115
  }
387116
+ /**
387117
+ * Render a `compaction_summary` user message folded into the model context
387118
+ * (the resume snapshot's context history carries it after compaction). The
387119
+ * message text is the model-facing summary; token counts are unknown here,
387120
+ * so only the summary is surfaced. The snapshot is still rebuilt from the
387121
+ * wire journal on resume (see `resumedAgentState`), so this branch is a
387122
+ * fallback for snapshots that were built from a folded context.
387123
+ */
387124
+ renderCompactionSummary(context, message) {
387125
+ this.flushAssistant(context);
387126
+ this.host.appendTranscriptEntry({
387127
+ ...replayEntry(context, "status", "Compaction complete", "plain"),
387128
+ compactionData: { summary: contentPartsToText(message.content) }
387129
+ });
387130
+ }
387039
387131
  renderHookResult(context, message) {
387040
387132
  if (message.origin?.kind !== "hook_result") return;
387041
387133
  this.flushAssistant(context);
@@ -387216,6 +387308,82 @@ var init_terminal_notification = __esmMin((() => {
387216
387308
  init_terminal();
387217
387309
  }));
387218
387310
  //#endregion
387311
+ //#region src/tui/utils/transcript-window.ts
387312
+ /**
387313
+ * Read a non-negative integer env var, falling back to `fallback` when it is
387314
+ * unset, empty, negative, or not an integer. `0` is a valid value (call sites
387315
+ * treat it as "feature disabled").
387316
+ */
387317
+ function readEnvInt(name, fallback) {
387318
+ const raw = process.env[name];
387319
+ if (raw === void 0 || raw.trim() === "") return fallback;
387320
+ const value = Number(raw);
387321
+ if (!Number.isInteger(value) || value < 0) return fallback;
387322
+ return value;
387323
+ }
387324
+ /**
387325
+ * Group consecutive entries into turns by `turnId`. Entries with the same
387326
+ * non-undefined `turnId` that are adjacent belong to the same turn.
387327
+ *
387328
+ * Entries with an undefined `turnId` are buffered and attached to the *next*
387329
+ * defined turn. This matters because a user message is appended (with
387330
+ * `turnId: undefined`) before its turn actually starts, so without this
387331
+ * buffering every user message would become its own single-entry turn at the
387332
+ * front and get trimmed first. Any undefined entries left at the tail (no
387333
+ * following turn) become their own turn.
387334
+ */
387335
+ function groupTurns(entries) {
387336
+ const turns = [];
387337
+ let current;
387338
+ let pendingUndefined = [];
387339
+ for (const entry of entries) {
387340
+ const turnId = entry.turnId;
387341
+ if (turnId === void 0) {
387342
+ pendingUndefined.push(entry);
387343
+ continue;
387344
+ }
387345
+ if (current !== void 0 && current.turnId === turnId) current.entries.push(entry);
387346
+ else {
387347
+ current = {
387348
+ turnId,
387349
+ entries: [...pendingUndefined, entry]
387350
+ };
387351
+ pendingUndefined = [];
387352
+ turns.push(current);
387353
+ }
387354
+ }
387355
+ if (pendingUndefined.length > 0) turns.push({
387356
+ turnId: void 0,
387357
+ entries: pendingUndefined
387358
+ });
387359
+ return turns;
387360
+ }
387361
+ /**
387362
+ * Decide which entries to destroy so the remaining turns fit within
387363
+ * `maxTurns`. Returns an empty set when the turn count is within
387364
+ * `maxTurns + hysteresis`. Oldest turns are removed first; the most recent
387365
+ * turn is never removed (it is the active / just-finished turn).
387366
+ */
387367
+ function turnsToTrim(turns, maxTurns, hysteresis) {
387368
+ const toRemove = /* @__PURE__ */ new Set();
387369
+ if (turns.length <= maxTurns + hysteresis) return toRemove;
387370
+ let remaining = turns.length;
387371
+ for (let i = 0; i < turns.length - 1 && remaining > maxTurns; i++) {
387372
+ const turn = turns[i];
387373
+ for (const entry of turn.entries) toRemove.add(entry);
387374
+ remaining--;
387375
+ }
387376
+ return toRemove;
387377
+ }
387378
+ var TRANSCRIPT_MAX_TURNS, TRANSCRIPT_EXPAND_TURNS, TRANSCRIPT_HYSTERESIS, TRANSCRIPT_KEEP_RECENT_STEPS, TRANSCRIPT_KEEP_TRAILING_TOOL_CALLS;
387379
+ var init_transcript_window = __esmMin((() => {
387380
+ TRANSCRIPT_MAX_TURNS = readEnvInt("DIMI_CODE_TUI_MAX_TURNS", 15);
387381
+ TRANSCRIPT_EXPAND_TURNS = readEnvInt("DIMI_CODE_TUI_EXPAND_TURNS", 3);
387382
+ TRANSCRIPT_HYSTERESIS = readEnvInt("DIMI_CODE_TUI_HYSTERESIS", 5);
387383
+ TRANSCRIPT_KEEP_RECENT_STEPS = readEnvInt("DIMI_CODE_TUI_KEEP_RECENT_STEPS", 30);
387384
+ TRANSCRIPT_KEEP_TRAILING_TOOL_CALLS = readEnvInt("DIMI_CODE_TUI_KEEP_TRAILING_TOOL_CALLS", 2);
387385
+ }));
387386
+ //#endregion
387219
387387
  //#region src/tui/controllers/streaming-ui.ts
387220
387388
  var StreamingUIController;
387221
387389
  var init_streaming_ui = __esmMin((() => {
@@ -387231,6 +387399,7 @@ var init_streaming_ui = __esmMin((() => {
387231
387399
  init_event_payload();
387232
387400
  init_terminal_notification();
387233
387401
  init_transcript_id();
387402
+ init_transcript_window();
387234
387403
  StreamingUIController = class {
387235
387404
  host;
387236
387405
  flushTimer;
@@ -387714,6 +387883,7 @@ var init_streaming_ui = __esmMin((() => {
387714
387883
  state.transcriptContainer.addChild(tc);
387715
387884
  state.ui.requestRender();
387716
387885
  }
387886
+ this.host.foldTrailingToolCalls(TRANSCRIPT_KEEP_TRAILING_TOOL_CALLS);
387717
387887
  if (toolCall.name === "ExitPlanMode" && typeof toolCall.args["plan"] !== "string") {
387718
387888
  const session = this.host.requireSession();
387719
387889
  (async () => {
@@ -390462,81 +390632,6 @@ var init_tmux_keyboard = __esmMin((() => {
390462
390632
  TMUX_EXTENDED_KEYS_FORMAT_XTERM_WARNING = "tmux extended-keys-format is xterm. Dimi works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
390463
390633
  }));
390464
390634
  //#endregion
390465
- //#region src/tui/utils/transcript-window.ts
390466
- /**
390467
- * Read a non-negative integer env var, falling back to `fallback` when it is
390468
- * unset, empty, negative, or not an integer. `0` is a valid value (call sites
390469
- * treat it as "feature disabled").
390470
- */
390471
- function readEnvInt(name, fallback) {
390472
- const raw = process.env[name];
390473
- if (raw === void 0 || raw.trim() === "") return fallback;
390474
- const value = Number(raw);
390475
- if (!Number.isInteger(value) || value < 0) return fallback;
390476
- return value;
390477
- }
390478
- /**
390479
- * Group consecutive entries into turns by `turnId`. Entries with the same
390480
- * non-undefined `turnId` that are adjacent belong to the same turn.
390481
- *
390482
- * Entries with an undefined `turnId` are buffered and attached to the *next*
390483
- * defined turn. This matters because a user message is appended (with
390484
- * `turnId: undefined`) before its turn actually starts, so without this
390485
- * buffering every user message would become its own single-entry turn at the
390486
- * front and get trimmed first. Any undefined entries left at the tail (no
390487
- * following turn) become their own turn.
390488
- */
390489
- function groupTurns(entries) {
390490
- const turns = [];
390491
- let current;
390492
- let pendingUndefined = [];
390493
- for (const entry of entries) {
390494
- const turnId = entry.turnId;
390495
- if (turnId === void 0) {
390496
- pendingUndefined.push(entry);
390497
- continue;
390498
- }
390499
- if (current !== void 0 && current.turnId === turnId) current.entries.push(entry);
390500
- else {
390501
- current = {
390502
- turnId,
390503
- entries: [...pendingUndefined, entry]
390504
- };
390505
- pendingUndefined = [];
390506
- turns.push(current);
390507
- }
390508
- }
390509
- if (pendingUndefined.length > 0) turns.push({
390510
- turnId: void 0,
390511
- entries: pendingUndefined
390512
- });
390513
- return turns;
390514
- }
390515
- /**
390516
- * Decide which entries to destroy so the remaining turns fit within
390517
- * `maxTurns`. Returns an empty set when the turn count is within
390518
- * `maxTurns + hysteresis`. Oldest turns are removed first; the most recent
390519
- * turn is never removed (it is the active / just-finished turn).
390520
- */
390521
- function turnsToTrim(turns, maxTurns, hysteresis) {
390522
- const toRemove = /* @__PURE__ */ new Set();
390523
- if (turns.length <= maxTurns + hysteresis) return toRemove;
390524
- let remaining = turns.length;
390525
- for (let i = 0; i < turns.length - 1 && remaining > maxTurns; i++) {
390526
- const turn = turns[i];
390527
- for (const entry of turn.entries) toRemove.add(entry);
390528
- remaining--;
390529
- }
390530
- return toRemove;
390531
- }
390532
- var TRANSCRIPT_MAX_TURNS, TRANSCRIPT_EXPAND_TURNS, TRANSCRIPT_HYSTERESIS, TRANSCRIPT_KEEP_RECENT_STEPS;
390533
- var init_transcript_window = __esmMin((() => {
390534
- TRANSCRIPT_MAX_TURNS = readEnvInt("DIMI_CODE_TUI_MAX_TURNS", 15);
390535
- TRANSCRIPT_EXPAND_TURNS = readEnvInt("DIMI_CODE_TUI_EXPAND_TURNS", 3);
390536
- TRANSCRIPT_HYSTERESIS = readEnvInt("DIMI_CODE_TUI_HYSTERESIS", 5);
390537
- TRANSCRIPT_KEEP_RECENT_STEPS = readEnvInt("DIMI_CODE_TUI_KEEP_RECENT_STEPS", 30);
390538
- }));
390539
- //#endregion
390540
390635
  //#region src/tui/dimi-tui.ts
390541
390636
  function loadingTipKind(mode) {
390542
390637
  if (mode === "waiting" || mode === "tool") return "moon";
@@ -391895,6 +391990,7 @@ var init_dimi_tui = __esmMin((() => {
391895
391990
  markTranscriptComponent(component, entry);
391896
391991
  this.state.transcriptContainer.addChild(component);
391897
391992
  }
391993
+ if (entry.kind === "tool_call") this.foldTrailingToolCalls(TRANSCRIPT_KEEP_TRAILING_TOOL_CALLS);
391898
391994
  const trimmed = this.trimTranscriptWindow();
391899
391995
  const merged = this.mergeCurrentTurnSteps();
391900
391996
  if (component || trimmed || merged) this.state.ui.requestRender();
@@ -391999,6 +392095,21 @@ var init_dimi_tui = __esmMin((() => {
391999
392095
  return this.foldCurrentTurnSteps(TRANSCRIPT_KEEP_RECENT_STEPS);
392000
392096
  }
392001
392097
  collapseTrailingToolCalls() {
392098
+ this.foldTrailingToolCalls(0);
392099
+ }
392100
+ /**
392101
+ * Fold the trailing contiguous run of tool calls, keeping the most recent
392102
+ * `keepExpanded` calls expanded. `keepExpanded = 0` collapses the whole run
392103
+ * (used when visible content follows, e.g. an assistant message or turn
392104
+ * end); a positive cap folds progressively while the run is still growing
392105
+ * so older calls merge into the summary immediately instead of staying
392106
+ * expanded until the run ends.
392107
+ *
392108
+ * Only finished calls fold — a call that is still running (streaming args,
392109
+ * a live subagent, a pending read) stays visible because its progress is
392110
+ * live information the user is watching.
392111
+ */
392112
+ foldTrailingToolCalls(keepExpanded) {
392002
392113
  const children = this.state.transcriptContainer.children;
392003
392114
  let start = children.length;
392004
392115
  const toolCalls = [];
@@ -392020,21 +392131,49 @@ var init_dimi_tui = __esmMin((() => {
392020
392131
  const standaloneNames = new Set(["AllDone", "WaitFor"]);
392021
392132
  const standaloneCalls = new Set(toolCalls.filter((call) => standaloneNames.has(call.toolCallView.name)));
392022
392133
  const sequenceCalls = toolCalls.filter((call) => !standaloneCalls.has(call));
392134
+ if (sequenceCalls.length === 0) return;
392135
+ const foldCount = sequenceCalls.length - keepExpanded;
392136
+ if (foldCount <= 0) return;
392023
392137
  const components = children.slice(start);
392024
392138
  const standalone = [];
392025
392139
  const sequenceComponents = [];
392140
+ const keptComponents = [];
392141
+ let remainingToFold = foldCount;
392142
+ let stoppedAtUnfinished = false;
392026
392143
  for (const component of components) {
392027
- if (component instanceof ToolCallSequenceComponent) {
392028
- sequenceComponents.push(...component.children);
392029
- component.clear();
392144
+ if (stoppedAtUnfinished) {
392145
+ keptComponents.push(component);
392030
392146
  continue;
392031
392147
  }
392032
- if (component instanceof ToolCallComponent && standaloneCalls.has(component)) standalone.push(component);
392033
- else sequenceComponents.push(component);
392034
- }
392035
- if (sequenceCalls.length === 0) return;
392036
- const sequence = new ToolCallSequenceComponent(sequenceComponents, sequenceCalls, this.state.toolDisplayMode);
392037
- children.splice(start, components.length, sequence, ...standalone);
392148
+ if (component instanceof ToolCallComponent && standaloneCalls.has(component)) {
392149
+ standalone.push(component);
392150
+ continue;
392151
+ }
392152
+ const calls = toolCallsIn(component);
392153
+ const callCount = calls?.filter((call) => !standaloneCalls.has(call)).length ?? 0;
392154
+ const hasUnfinished = calls?.some((call) => !call.isFinished()) ?? false;
392155
+ if (remainingToFold > 0 && hasUnfinished) {
392156
+ keptComponents.push(component);
392157
+ stoppedAtUnfinished = true;
392158
+ continue;
392159
+ }
392160
+ if (remainingToFold > 0) {
392161
+ if (component instanceof ToolCallSequenceComponent) {
392162
+ sequenceComponents.push(...component.children);
392163
+ component.clear();
392164
+ } else sequenceComponents.push(component);
392165
+ remainingToFold -= callCount;
392166
+ } else keptComponents.push(component);
392167
+ }
392168
+ if (sequenceComponents.length === 0) return;
392169
+ const foldedCalls = [];
392170
+ for (const component of sequenceComponents) {
392171
+ const componentCalls = toolCallsIn(component);
392172
+ if (componentCalls !== void 0) foldedCalls.push(...componentCalls.filter((call) => !standaloneCalls.has(call)));
392173
+ }
392174
+ if (foldedCalls.length === 0) return;
392175
+ const sequence = new ToolCallSequenceComponent(sequenceComponents, foldedCalls, this.state.toolDisplayMode);
392176
+ children.splice(start, components.length, sequence, ...standalone, ...keptComponents);
392038
392177
  this.state.transcriptContainer.invalidate();
392039
392178
  }
392040
392179
  foldCurrentTurnSteps(keepSteps) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimi-agent/cli",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "The Starting Point for Next-Gen Agents",
5
5
  "keywords": [
6
6
  "agent",
@@ -60,12 +60,12 @@
60
60
  "tsx": "^4.21.0",
61
61
  "yazl": "^3.3.1",
62
62
  "zod": "^4.3.6",
63
- "@dimi-agent/agent-core-v2": "^0.1.0",
63
+ "@dimi-agent/dimi-oauth": "^0.1.0",
64
64
  "@dimi-agent/dimi-telemetry": "^0.1.0",
65
+ "@dimi-agent/agent-core-v2": "^0.1.0",
65
66
  "@dimi-agent/dimi-sdk": "^0.2.0",
66
67
  "@dimi-agent/dimi-web": "^0.1.0",
67
68
  "@dimi-agent/kap-server": "^0.1.0",
68
- "@dimi-agent/dimi-oauth": "^0.1.0",
69
69
  "@dimi-agent/pi-tui": "^0.1.0",
70
70
  "@dimi-agent/remote": "^0.1.0"
71
71
  },