@vybestack/llxprt-code-core 0.10.0 → 0.11.0-nightly.260728.ffaa8d5d7

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 (168) hide show
  1. package/dist/src/config/config.d.ts +20 -0
  2. package/dist/src/config/config.js +143 -23
  3. package/dist/src/config/config.js.map +1 -1
  4. package/dist/src/config/configBaseCore.d.ts +6 -2
  5. package/dist/src/config/configBaseCore.js +8 -0
  6. package/dist/src/config/configBaseCore.js.map +1 -1
  7. package/dist/src/config/configConstructor.d.ts +2 -0
  8. package/dist/src/config/configConstructor.js +9 -1
  9. package/dist/src/config/configConstructor.js.map +1 -1
  10. package/dist/src/config/configTestHarness.d.ts +8 -6
  11. package/dist/src/config/configTestHarness.js +10 -4
  12. package/dist/src/config/configTestHarness.js.map +1 -1
  13. package/dist/src/config/configTypes.d.ts +7 -2
  14. package/dist/src/config/configTypes.js +2 -0
  15. package/dist/src/config/configTypes.js.map +1 -1
  16. package/dist/src/config/liveTrustTransitionLifecycle.d.ts +35 -0
  17. package/dist/src/config/liveTrustTransitionLifecycle.js +180 -0
  18. package/dist/src/config/liveTrustTransitionLifecycle.js.map +1 -0
  19. package/dist/src/config/schedulerSingleton.d.ts +2 -2
  20. package/dist/src/config/schedulerSingleton.js.map +1 -1
  21. package/dist/src/core/compression/types.d.ts +48 -1
  22. package/dist/src/core/compression/types.js +41 -0
  23. package/dist/src/core/compression/types.js.map +1 -1
  24. package/dist/src/core/model-limits.json +96 -0
  25. package/dist/src/core/model-limits.schema.d.ts +123 -0
  26. package/dist/src/core/model-limits.schema.js +75 -0
  27. package/dist/src/core/model-limits.schema.js.map +1 -0
  28. package/dist/src/core/tokenLimits.d.ts +6 -8
  29. package/dist/src/core/tokenLimits.js +56 -132
  30. package/dist/src/core/tokenLimits.js.map +1 -1
  31. package/dist/src/core/turn.d.ts +20 -3
  32. package/dist/src/core/turn.js +20 -3
  33. package/dist/src/core/turn.js.map +1 -1
  34. package/dist/src/hooks/hookRegistry.d.ts +1 -1
  35. package/dist/src/hooks/hookRegistry.js +3 -1
  36. package/dist/src/hooks/hookRegistry.js.map +1 -1
  37. package/dist/src/hooks/hookRunner.d.ts +6 -0
  38. package/dist/src/hooks/hookRunner.js +22 -6
  39. package/dist/src/hooks/hookRunner.js.map +1 -1
  40. package/dist/src/hooks/hookSystem.d.ts +10 -1
  41. package/dist/src/hooks/hookSystem.js +69 -12
  42. package/dist/src/hooks/hookSystem.js.map +1 -1
  43. package/dist/src/index.d.ts +1 -0
  44. package/dist/src/index.js +1 -0
  45. package/dist/src/index.js.map +1 -1
  46. package/dist/src/llm-types/geminiContent.d.ts +3 -0
  47. package/dist/src/policy/config.d.ts +4 -2
  48. package/dist/src/policy/config.js +22 -18
  49. package/dist/src/policy/config.js.map +1 -1
  50. package/dist/src/recording/ReplayEngine.js +39 -2
  51. package/dist/src/recording/ReplayEngine.js.map +1 -1
  52. package/dist/src/recording/SessionDiscovery.d.ts +8 -0
  53. package/dist/src/recording/SessionDiscovery.js +88 -3
  54. package/dist/src/recording/SessionDiscovery.js.map +1 -1
  55. package/dist/src/recording/SessionLockManager.d.ts +3 -0
  56. package/dist/src/recording/SessionLockManager.js +9 -3
  57. package/dist/src/recording/SessionLockManager.js.map +1 -1
  58. package/dist/src/recording/SessionRecordingService.d.ts +14 -1
  59. package/dist/src/recording/SessionRecordingService.js +27 -3
  60. package/dist/src/recording/SessionRecordingService.js.map +1 -1
  61. package/dist/src/recording/index.d.ts +4 -3
  62. package/dist/src/recording/index.js +4 -3
  63. package/dist/src/recording/index.js.map +1 -1
  64. package/dist/src/recording/replay-test-helpers.d.ts +5 -0
  65. package/dist/src/recording/replay-test-helpers.js +13 -0
  66. package/dist/src/recording/replay-test-helpers.js.map +1 -1
  67. package/dist/src/recording/resumeNotFoundMessages.d.ts +56 -0
  68. package/dist/src/recording/resumeNotFoundMessages.js +64 -0
  69. package/dist/src/recording/resumeNotFoundMessages.js.map +1 -0
  70. package/dist/src/recording/resumeSession.js +3 -2
  71. package/dist/src/recording/resumeSession.js.map +1 -1
  72. package/dist/src/recording/sessionManagement.d.ts +3 -0
  73. package/dist/src/recording/sessionManagement.js +38 -19
  74. package/dist/src/recording/sessionManagement.js.map +1 -1
  75. package/dist/src/recording/types.d.ts +42 -3
  76. package/dist/src/recording/types.js +1 -1
  77. package/dist/src/recording/types.js.map +1 -1
  78. package/dist/src/runtime/AgentRuntimeLoader.js +2 -1
  79. package/dist/src/runtime/AgentRuntimeLoader.js.map +1 -1
  80. package/dist/src/scheduler/liveOutput.d.ts +10 -19
  81. package/dist/src/scheduler/liveOutput.js +21 -21
  82. package/dist/src/scheduler/liveOutput.js.map +1 -1
  83. package/dist/src/scheduler/types.d.ts +2 -2
  84. package/dist/src/services/history/ContentConverters.d.ts +4 -0
  85. package/dist/src/services/history/ContentConverters.js +77 -59
  86. package/dist/src/services/history/ContentConverters.js.map +1 -1
  87. package/dist/src/services/history/HistoryService.d.ts +13 -1
  88. package/dist/src/services/history/HistoryService.js +71 -0
  89. package/dist/src/services/history/HistoryService.js.map +1 -1
  90. package/dist/src/services/history/IContent.d.ts +10 -0
  91. package/dist/src/services/history/IContent.js.map +1 -1
  92. package/dist/src/skills/skillManager.d.ts +11 -1
  93. package/dist/src/skills/skillManager.js +21 -1
  94. package/dist/src/skills/skillManager.js.map +1 -1
  95. package/dist/src/test-utils/mock-tool.d.ts +3 -3
  96. package/dist/src/test-utils/mock-tool.js.map +1 -1
  97. package/dist/src/test-utils/tools.d.ts +2 -2
  98. package/dist/src/test-utils/tools.js.map +1 -1
  99. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js +1 -6
  100. package/dist/src/tools-adapters/CoreShellToolHostAdapter.js.map +1 -1
  101. package/dist/src/utils/browser-profile-discovery.d.ts +40 -0
  102. package/dist/src/utils/browser-profile-discovery.js +251 -0
  103. package/dist/src/utils/browser-profile-discovery.js.map +1 -0
  104. package/dist/src/utils/env.d.ts +12 -0
  105. package/dist/src/utils/env.js +15 -0
  106. package/dist/src/utils/env.js.map +1 -0
  107. package/dist/src/utils/events.d.ts +10 -1
  108. package/dist/src/utils/events.js +22 -0
  109. package/dist/src/utils/events.js.map +1 -1
  110. package/dist/src/utils/fileUtils.js +8 -1
  111. package/dist/src/utils/fileUtils.js.map +1 -1
  112. package/dist/src/utils/retry.js +3 -4
  113. package/dist/src/utils/retry.js.map +1 -1
  114. package/dist/src/utils/secure-browser-launcher.d.ts +35 -1
  115. package/dist/src/utils/secure-browser-launcher.js +341 -1
  116. package/dist/src/utils/secure-browser-launcher.js.map +1 -1
  117. package/dist/src/utils/terminalSerializer.d.ts +12 -0
  118. package/dist/src/utils/terminalSerializer.js.map +1 -1
  119. package/dist/src/utils/thoughtUtils.d.ts +2 -0
  120. package/dist/src/utils/thoughtUtils.js.map +1 -1
  121. package/package.json +13 -9
  122. package/src/config/config.ts +161 -22
  123. package/src/config/configBaseCore.ts +10 -2
  124. package/src/config/configConstructor.ts +11 -0
  125. package/src/config/configTestHarness.ts +10 -4
  126. package/src/config/configTypes.ts +6 -1
  127. package/src/config/liveTrustTransitionLifecycle.ts +250 -0
  128. package/src/config/schedulerSingleton.ts +2 -5
  129. package/src/core/compression/types.ts +81 -1
  130. package/src/core/legacy-model-limits.expected.txt +96 -0
  131. package/src/core/model-limits.json +96 -0
  132. package/src/core/model-limits.schema.ts +87 -0
  133. package/src/core/tokenLimits.ts +73 -147
  134. package/src/core/turn.ts +20 -3
  135. package/src/hooks/hookRegistry.ts +3 -1
  136. package/src/hooks/hookRunner.ts +24 -7
  137. package/src/hooks/hookSystem.ts +73 -13
  138. package/src/index.ts +1 -0
  139. package/src/llm-types/geminiContent.ts +3 -0
  140. package/src/policy/config.ts +32 -19
  141. package/src/recording/ReplayEngine.ts +51 -2
  142. package/src/recording/SessionDiscovery.ts +99 -4
  143. package/src/recording/SessionLockManager.ts +10 -3
  144. package/src/recording/SessionRecordingService.ts +36 -3
  145. package/src/recording/index.ts +19 -2
  146. package/src/recording/replay-test-helpers.ts +14 -0
  147. package/src/recording/resumeNotFoundMessages.ts +75 -0
  148. package/src/recording/resumeSession.ts +3 -1
  149. package/src/recording/sessionManagement.ts +51 -21
  150. package/src/recording/types.ts +44 -2
  151. package/src/runtime/AgentRuntimeLoader.ts +3 -1
  152. package/src/scheduler/liveOutput.ts +25 -22
  153. package/src/scheduler/types.ts +5 -2
  154. package/src/services/history/ContentConverters.ts +91 -68
  155. package/src/services/history/HistoryService.ts +92 -1
  156. package/src/services/history/IContent.ts +13 -0
  157. package/src/skills/skillManager.ts +26 -1
  158. package/src/test-utils/mock-tool.ts +4 -3
  159. package/src/test-utils/tools.ts +4 -3
  160. package/src/tools-adapters/CoreShellToolHostAdapter.ts +1 -7
  161. package/src/utils/browser-profile-discovery.ts +358 -0
  162. package/src/utils/env.ts +15 -0
  163. package/src/utils/events.ts +31 -0
  164. package/src/utils/fileUtils.ts +11 -1
  165. package/src/utils/retry.ts +3 -4
  166. package/src/utils/secure-browser-launcher.ts +500 -2
  167. package/src/utils/terminalSerializer.ts +9 -0
  168. package/src/utils/thoughtUtils.ts +2 -0
@@ -25,12 +25,14 @@
25
25
 
26
26
  import { type IContent } from '../services/history/IContent.js';
27
27
 
28
+ export const SESSION_TITLE_MAX_LENGTH = 120;
29
+
28
30
  // ---------------------------------------------------------------------------
29
31
  // Event type discriminator
30
32
  // ---------------------------------------------------------------------------
31
33
 
32
34
  /**
33
- * The seven event types that can appear in a session JSONL file.
35
+ * The event types that can appear in a session JSONL file.
34
36
  */
35
37
  export type SessionEventType =
36
38
  | 'session_start'
@@ -39,6 +41,7 @@ export type SessionEventType =
39
41
  | 'rewind'
40
42
  | 'provider_switch'
41
43
  | 'session_event'
44
+ | 'session_metadata'
42
45
  | 'directories_changed';
43
46
 
44
47
  // ---------------------------------------------------------------------------
@@ -74,6 +77,8 @@ export interface SessionStartPayload {
74
77
  sessionId: string;
75
78
  projectHash: string;
76
79
  workspaceDirs: string[];
80
+ /** Effective absolute working directory, when recorded by newer clients. */
81
+ cwd?: string;
77
82
  provider: string;
78
83
  model: string;
79
84
  /** ISO-8601 timestamp of when the session started. */
@@ -122,6 +127,19 @@ export interface SessionEventPayload {
122
127
  message: string;
123
128
  }
124
129
 
130
+ /**
131
+ * Payload for the `session_metadata` event — session-level metadata updates
132
+ * (currently only the human-readable title). The title is tri-state:
133
+ * - `undefined` (field absent): legacy event, no title assertion.
134
+ * - `null`: explicit untitled (the caller asserts there is no meaningful title).
135
+ * - `string`: a concrete title.
136
+ *
137
+ * This is a typed first-class recording event, NOT a `session_event` sentinel.
138
+ */
139
+ export interface SessionMetadataPayload {
140
+ readonly title?: string | null;
141
+ }
142
+
125
143
  /**
126
144
  * Payload for the `directories_changed` event.
127
145
  */
@@ -141,6 +159,8 @@ export interface SessionRecordingServiceConfig {
141
159
  projectHash: string;
142
160
  chatsDir: string;
143
161
  workspaceDirs: string[];
162
+ /** Effective absolute working directory for lifecycle discovery. */
163
+ cwd?: string;
144
164
  provider: string;
145
165
  model: string;
146
166
  }
@@ -151,7 +171,14 @@ export interface SessionRecordingServiceConfig {
151
171
 
152
172
  /**
153
173
  * Metadata extracted from a session's `session_start` event and updated
154
- * by subsequent `provider_switch` / `directories_changed` events.
174
+ * by subsequent `provider_switch` / `directories_changed` / `session_metadata`
175
+ * events.
176
+ *
177
+ * The title is tri-state:
178
+ * - `undefined`: no `session_metadata` event seen (legacy file). Callers fall
179
+ * back to first-human-text heuristics.
180
+ * - `null`: explicit untitled (`session_metadata` asserted no title).
181
+ * - `string`: a concrete title.
155
182
  */
156
183
  export interface SessionMetadata {
157
184
  sessionId: string;
@@ -159,7 +186,9 @@ export interface SessionMetadata {
159
186
  provider: string;
160
187
  model: string;
161
188
  workspaceDirs: string[];
189
+ cwd?: string;
162
190
  startTime: string;
191
+ title?: string | null;
163
192
  }
164
193
 
165
194
  /**
@@ -199,4 +228,17 @@ export interface SessionSummary {
199
228
  fileSize: number;
200
229
  provider: string;
201
230
  model: string;
231
+ cwd?: string;
232
+ /**
233
+ * Tri-state title from a persisted `session_metadata` event:
234
+ * - `undefined`: no `session_metadata` event seen (legacy fallback).
235
+ * - `null`: explicit untitled.
236
+ * - `string`: a concrete title.
237
+ */
238
+ title?: string | null;
239
+ /**
240
+ * Immutable creation timestamp extracted from `session_start.startTime`.
241
+ * Used for stable ordering independent of file `lastModified` mutations.
242
+ */
243
+ createdAt?: string;
202
244
  }
@@ -109,6 +109,7 @@ function hydrateContentGeneratorConfig(
109
109
 
110
110
  type ToolGovernance = {
111
111
  allowed: Set<string>;
112
+ allowedExplicit: boolean;
112
113
  disabled: Set<string>;
113
114
  excluded: Set<string>;
114
115
  };
@@ -128,6 +129,7 @@ function buildToolGovernance(
128
129
  allowed: new Set(
129
130
  (allowedRaw ?? []).map((tool) => normalizeToolName(tool) ?? tool),
130
131
  ),
132
+ allowedExplicit: allowedRaw !== undefined,
131
133
  disabled: new Set(
132
134
  (disabledRaw ?? []).map((tool) => normalizeToolName(tool) ?? tool),
133
135
  ),
@@ -148,7 +150,7 @@ function isToolPermitted(
148
150
  if (governance.disabled.has(canonical)) {
149
151
  return false;
150
152
  }
151
- if (governance.allowed.size > 0 && !governance.allowed.has(canonical)) {
153
+ if (governance.allowedExplicit && !governance.allowed.has(canonical)) {
152
154
  return false;
153
155
  }
154
156
  return true;
@@ -4,36 +4,39 @@
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
6
 
7
- import type { AnsiOutput } from '../utils/terminalSerializer.js';
7
+ import type {
8
+ AnsiOutput,
9
+ LiveOutputUpdate,
10
+ } from '../utils/terminalSerializer.js';
8
11
 
9
12
  /**
10
- * Accumulates a live-output chunk onto an existing value.
13
+ * Accumulates a live-output update onto an existing accumulated value using
14
+ * the explicit tagged {@link LiveOutputUpdate} protocol.
11
15
  *
12
- * Tools that stream via `canUpdateOutput` deliver chunks in one of two
13
- * semantics:
14
- *
15
- * - **`AnsiOutput`** (e.g. the shell tool) — a full terminal-buffer snapshot.
16
- * Each chunk supersedes the previous one, so the latest snapshot replaces
17
- * the old value.
18
- * - **`string`** (e.g. the task tool streaming subagent text) — an incremental
19
- * delta. Each chunk must be appended to the existing text so the display
20
- * grows rather than showing only the latest token.
16
+ * - **`append`** (text deltas) concatenated onto the existing value when it
17
+ * is a string; otherwise the prior non-string value is dropped and the
18
+ * delta starts a fresh string.
19
+ * - **`replace`** (terminal-buffer snapshots) — supersedes any prior value.
21
20
  *
22
21
  * The `existing` parameter is typed `unknown` because callers may pass values
23
22
  * drawn from a broader display union (e.g. `ToolResultDisplay`, which also
24
- * includes `FileDiff` / `FileRead`). The function only ever appends when both
25
- * values are strings; any other combination replaces with the incoming chunk.
26
- *
27
- * Note: the append-vs-replace distinction is currently inferred from the
28
- * payload type. Migrating to an explicit tagged update protocol (e.g.
29
- * `{ mode: 'append'; data: string } | { mode: 'replace'; data: AnsiOutput }`)
30
- * is tracked in follow-up #2586.
23
+ * includes `FileDiff` / `FileRead`). The returned type is the narrow
24
+ * `string | AnsiOutput` union the live-output channel actually produces.
31
25
  */
32
26
  export function accumulateLiveOutput(
33
27
  existing: unknown,
34
- chunk: string | AnsiOutput,
28
+ update: LiveOutputUpdate,
35
29
  ): string | AnsiOutput {
36
- return typeof chunk === 'string' && typeof existing === 'string'
37
- ? existing + chunk
38
- : chunk;
30
+ switch (update.mode) {
31
+ case 'append':
32
+ return typeof existing === 'string'
33
+ ? existing + update.data
34
+ : update.data;
35
+ case 'replace':
36
+ return update.data;
37
+ default: {
38
+ const _exhaustive: never = update;
39
+ return _exhaustive;
40
+ }
41
+ }
39
42
  }
@@ -22,7 +22,10 @@ import type {
22
22
  ToolCallResponseInfo,
23
23
  } from '../core/turn.js';
24
24
  import type { ToolConfirmationOutcome } from '@vybestack/llxprt-code-tools';
25
- import type { AnsiOutput } from '../utils/terminalSerializer.js';
25
+ import type {
26
+ AnsiOutput,
27
+ LiveOutputUpdate,
28
+ } from '../utils/terminalSerializer.js';
26
29
  import type { SerializableConfirmationDetails } from '../confirmation-bus/types.js';
27
30
 
28
31
  /**
@@ -142,7 +145,7 @@ export type ConfirmHandler = (
142
145
 
143
146
  export type OutputUpdateHandler = (
144
147
  toolCallId: string,
145
- outputChunk: string | AnsiOutput,
148
+ update: LiveOutputUpdate,
146
149
  ) => void;
147
150
 
148
151
  export type AllToolCallsCompleteHandler = (
@@ -73,77 +73,92 @@ export class ContentConverters {
73
73
  return 'model';
74
74
  }
75
75
 
76
+ private static toolCallBlockToPart(
77
+ toolCall: Extract<ContentBlock, { type: 'tool_call' }>,
78
+ ): GeminiContentPart {
79
+ this.logger.debug('Converting tool_call block to functionCall:', {
80
+ id: toolCall.id,
81
+ name: toolCall.name,
82
+ hasParameters: ContentConverters.hasLegacyTruthyValue(
83
+ toolCall.parameters,
84
+ ),
85
+ });
86
+ return {
87
+ functionCall: {
88
+ name: toolCall.name,
89
+ args: toolCall.parameters as Record<string, unknown>,
90
+ id: toolCall.id,
91
+ },
92
+ };
93
+ }
94
+
95
+ private static toolResponseBlockToPart(
96
+ toolResponse: Extract<ContentBlock, { type: 'tool_response' }>,
97
+ ): GeminiContentPart {
98
+ this.logger.debug('Converting tool_response block to functionResponse:', {
99
+ callId: toolResponse.callId,
100
+ toolName: toolResponse.toolName,
101
+ hasResult: ContentConverters.hasLegacyTruthyValue(toolResponse.result),
102
+ hasError: ContentConverters.hasLegacyTruthyValue(toolResponse.error),
103
+ });
104
+ return {
105
+ functionResponse: {
106
+ name: toolResponse.toolName,
107
+ response: toolResponse.result as Record<string, unknown>,
108
+ id: toolResponse.callId,
109
+ },
110
+ };
111
+ }
112
+
113
+ private static thinkingBlockToPart(
114
+ thinkingBlock: Extract<ContentBlock, { type: 'thinking' }>,
115
+ ): GeminiContentPart {
116
+ const thinkingPart: GeminiContentPart = {
117
+ thought: true,
118
+ text: thinkingBlock.thought,
119
+ };
120
+ if (ContentConverters.hasLegacyTruthyValue(thinkingBlock.signature)) {
121
+ thinkingPart.thoughtSignature = thinkingBlock.signature;
122
+ }
123
+ if (ContentConverters.hasLegacyTruthyValue(thinkingBlock.sourceField)) {
124
+ thinkingPart.llxprtSourceField = thinkingBlock.sourceField;
125
+ }
126
+ if (ContentConverters.hasLegacyTruthyValue(thinkingBlock.streamId)) {
127
+ thinkingPart.llxprtThoughtBlockId = thinkingBlock.streamId;
128
+ }
129
+ if (ContentConverters.hasLegacyTruthyValue(thinkingBlock.streamStatus)) {
130
+ thinkingPart.llxprtThoughtBlockStatus = thinkingBlock.streamStatus;
131
+ }
132
+ if (thinkingBlock.isHidden !== undefined) {
133
+ thinkingPart.llxprtThoughtIsHidden = thinkingBlock.isHidden;
134
+ }
135
+ return thinkingPart;
136
+ }
137
+
138
+ private static codeBlockToPart(
139
+ codeBlock: Extract<ContentBlock, { type: 'code' }>,
140
+ ): GeminiContentPart {
141
+ const codeText = codeBlock.language
142
+ ? `\`\`\`${codeBlock.language}\n${codeBlock.code}\n\`\`\``
143
+ : codeBlock.code;
144
+ return { text: codeText };
145
+ }
146
+
76
147
  /** Convert a single IContent block to a Gemini Part. */
77
148
  private static blockToPart(block: ContentBlock): GeminiContentPart | null {
78
149
  switch (block.type) {
79
- case 'text': {
80
- const textBlock = block;
81
- return { text: textBlock.text };
82
- }
83
- case 'tool_call': {
84
- const toolCall = block;
85
- this.logger.debug('Converting tool_call block to functionCall:', {
86
- id: toolCall.id,
87
- name: toolCall.name,
88
- hasParameters: ContentConverters.hasLegacyTruthyValue(
89
- toolCall.parameters,
90
- ),
91
- });
92
- return {
93
- functionCall: {
94
- name: toolCall.name,
95
- args: toolCall.parameters as Record<string, unknown>,
96
- id: toolCall.id,
97
- },
98
- };
99
- }
100
- case 'tool_response': {
101
- const toolResponse = block;
102
- this.logger.debug(
103
- 'Converting tool_response block to functionResponse:',
104
- {
105
- callId: toolResponse.callId,
106
- toolName: toolResponse.toolName,
107
- hasResult: ContentConverters.hasLegacyTruthyValue(
108
- toolResponse.result,
109
- ),
110
- hasError: ContentConverters.hasLegacyTruthyValue(
111
- toolResponse.error,
112
- ),
113
- },
114
- );
115
- return {
116
- functionResponse: {
117
- name: toolResponse.toolName,
118
- response: toolResponse.result as Record<string, unknown>,
119
- id: toolResponse.callId,
120
- },
121
- };
122
- }
123
- case 'thinking': {
124
- const thinkingBlock = block;
125
- const thinkingPart: GeminiContentPart = {
126
- thought: true,
127
- text: thinkingBlock.thought,
128
- };
129
- if (ContentConverters.hasLegacyTruthyValue(thinkingBlock.signature)) {
130
- thinkingPart.thoughtSignature = thinkingBlock.signature;
131
- }
132
- if (ContentConverters.hasLegacyTruthyValue(thinkingBlock.sourceField)) {
133
- thinkingPart.llxprtSourceField = thinkingBlock.sourceField;
134
- }
135
- return thinkingPart;
136
- }
137
- case 'media': {
150
+ case 'text':
151
+ return { text: block.text };
152
+ case 'tool_call':
153
+ return this.toolCallBlockToPart(block);
154
+ case 'tool_response':
155
+ return this.toolResponseBlockToPart(block);
156
+ case 'thinking':
157
+ return this.thinkingBlockToPart(block);
158
+ case 'media':
138
159
  return null;
139
- }
140
- case 'code': {
141
- const codeBlock = block;
142
- const codeText = codeBlock.language
143
- ? `\`\`\`${codeBlock.language}\n${codeBlock.code}\n\`\`\``
144
- : codeBlock.code;
145
- return { text: codeText };
146
- }
160
+ case 'code':
161
+ return this.codeBlockToPart(block);
147
162
  default:
148
163
  return null;
149
164
  }
@@ -201,12 +216,20 @@ export class ContentConverters {
201
216
  const thinkingBlock: ThinkingBlock = {
202
217
  type: 'thinking',
203
218
  thought: part.text ?? '',
204
- isHidden: true,
205
219
  sourceField,
206
220
  };
221
+ if (part.llxprtThoughtIsHidden !== undefined) {
222
+ thinkingBlock.isHidden = part.llxprtThoughtIsHidden;
223
+ }
207
224
  if (part.thoughtSignature) {
208
225
  thinkingBlock.signature = part.thoughtSignature;
209
226
  }
227
+ if (part.llxprtThoughtBlockId) {
228
+ thinkingBlock.streamId = part.llxprtThoughtBlockId;
229
+ }
230
+ if (part.llxprtThoughtBlockStatus) {
231
+ thinkingBlock.streamStatus = part.llxprtThoughtBlockStatus;
232
+ }
210
233
  return thinkingBlock;
211
234
  }
212
235
 
@@ -14,7 +14,11 @@
14
14
  * limitations under the License.
15
15
  */
16
16
 
17
- import { type IContent, type ToolCallBlock } from './IContent.js';
17
+ import {
18
+ type IContent,
19
+ type ToolCallBlock,
20
+ type ToolResponseBlock,
21
+ } from './IContent.js';
18
22
  import { EventEmitter } from 'events';
19
23
  // @plan:PLAN-20260603-ISSUE1584.P05 RuntimeTokenizerFactory used for injection path
20
24
  import type { RuntimeTokenizerFactory } from '../../runtime/contracts/RuntimeTokenizerFactory.js';
@@ -437,6 +441,93 @@ export class HistoryService
437
441
  await this.recalculateTotalTokens();
438
442
  }
439
443
 
444
+ /**
445
+ * Immutably replace a single tool_response block with a replacement
446
+ * tool_response block, preserving callId/toolName invariants.
447
+ *
448
+ * Unlike a generic block-replacement API, this method enforces structural
449
+ * invariants that compression/truncation callers rely on:
450
+ * - The target block at (entryIndex, blockIndex) MUST be a tool_response.
451
+ * - The replacement MUST be a tool_response.
452
+ * - The replacement MUST carry the same callId and toolName as the target.
453
+ * This prevents accidental corruption of tool-call/response pairing.
454
+ */
455
+ async replaceToolResponseBlock(
456
+ entryIndex: number,
457
+ blockIndex: number,
458
+ replacement: ToolResponseBlock,
459
+ modelName?: string,
460
+ ): Promise<boolean> {
461
+ if (
462
+ !Number.isInteger(entryIndex) ||
463
+ entryIndex < 0 ||
464
+ entryIndex >= this.history.length
465
+ ) {
466
+ return false;
467
+ }
468
+ const entry = this.history[entryIndex];
469
+ if (
470
+ !Number.isInteger(blockIndex) ||
471
+ blockIndex < 0 ||
472
+ blockIndex >= entry.blocks.length
473
+ ) {
474
+ return false;
475
+ }
476
+
477
+ const target = entry.blocks[blockIndex];
478
+ if (target.type !== 'tool_response') {
479
+ return false;
480
+ }
481
+ // Runtime invariant: the replacement MUST be a tool_response at runtime,
482
+ // even though the TypeScript type already constrains it. A malformed
483
+ // object with matching callId/toolName but wrong type (or missing type)
484
+ // could slip through at runtime and corrupt tool-call/response pairing.
485
+ const replacementType = (replacement as { type?: unknown }).type;
486
+ if (replacementType !== 'tool_response') {
487
+ return false;
488
+ }
489
+ if (
490
+ target.callId !== replacement.callId ||
491
+ target.toolName !== replacement.toolName
492
+ ) {
493
+ return false;
494
+ }
495
+
496
+ const newBlocks = [...entry.blocks];
497
+ newBlocks[blockIndex] = replacement;
498
+ const previousEntry = this.history[entryIndex];
499
+ const previousTotalTokens = this.totalTokens;
500
+ this.history[entryIndex] = { ...entry, blocks: newBlocks };
501
+
502
+ try {
503
+ await this.recalculateTotalTokens(modelName);
504
+ } catch (error) {
505
+ // Restore BOTH invariants: the history array AND the token accounting.
506
+ // recalculateTotalTokens may have already mutated this.totalTokens to
507
+ // reflect the replacement content before a listener/event error aborted
508
+ // the emit. Leaving totalTokens stale would corrupt the token budget.
509
+ this.history[entryIndex] = previousEntry;
510
+ this.totalTokens = previousTotalTokens;
511
+ // Best-effort notification so healthy listeners observe the rollback.
512
+ // A broken listener that originally caused the failure must not mask
513
+ // the original error.
514
+ try {
515
+ this.emit('tokensUpdated', {
516
+ totalTokens: this.getTotalTokens(),
517
+ addedTokens: 0,
518
+ contentId: null,
519
+ });
520
+ } catch (emitError) {
521
+ this.logger.debug(
522
+ 'tokensUpdated emit during rollback failed; original error preserved',
523
+ emitError,
524
+ );
525
+ }
526
+ throw error;
527
+ }
528
+ return true;
529
+ }
530
+
440
531
  /**
441
532
  * Return a read-only typed view of the backing history array.
442
533
  *
@@ -55,6 +55,13 @@ export interface ContentMetadata {
55
55
  /** Unique identifier for this content */
56
56
  id?: string;
57
57
 
58
+ /**
59
+ * Whether this AI turn was persisted server-side (e.g. OpenAI Responses
60
+ * API store=true). When true the id is safe to reference via
61
+ * previous_response_id for stateful conversations (#207).
62
+ */
63
+ responsesStored?: boolean;
64
+
58
65
  /** Provider that generated this content */
59
66
  provider?: string;
60
67
 
@@ -244,6 +251,12 @@ export interface ThinkingBlock {
244
251
  /** Signature for Anthropic extended thinking */
245
252
  signature?: string;
246
253
 
254
+ /** Provider-scoped stream identity for replacing incremental thinking updates */
255
+ streamId?: string;
256
+
257
+ /** Whether this block is an incremental update or the completed thinking block */
258
+ streamStatus?: 'delta' | 'complete';
259
+
247
260
  /** Base64-encoded reasoning content (for OpenAI Codex/Responses API) */
248
261
  encryptedContent?: string;
249
262
 
@@ -108,7 +108,17 @@ export class SkillManager {
108
108
 
109
109
  /**
110
110
  * Discovers skills from built-in, extension, user and workspace locations.
111
- * Precedence: Built-in (lowest) -> Extensions -> User -> Workspace (highest).
111
+ *
112
+ * Load order (later loads win for same-named skills):
113
+ * 1. Built-in
114
+ * 2. Extensions
115
+ * 3. User `<OS-standard-config-dir>/skills/`
116
+ * 4. User `~/.agents/skills/` (cross-tool standard, https://agentskills.io)
117
+ * 5. Workspace `.llxprt/skills/`
118
+ * 6. Workspace `.agents/skills/` (highest precedence)
119
+ *
120
+ * The overall Workspace > User > Extension > Built-in precedence is
121
+ * preserved, while `.agents/skills/` wins within each user or workspace tier.
112
122
  */
113
123
  async discoverSkills(
114
124
  storage: Storage,
@@ -143,6 +153,16 @@ export class SkillManager {
143
153
  'user',
144
154
  );
145
155
  this.addSkillsWithPrecedence(userSkills);
156
+ try {
157
+ const userAgentSkills = await loadSkillsFromDir(
158
+ Storage.getUserAgentSkillsDir(),
159
+ 'user',
160
+ );
161
+ this.addSkillsWithPrecedence(userAgentSkills);
162
+ } catch (error) {
163
+ const reason = error instanceof Error ? error.message : String(error);
164
+ debugLogger.warn(`Skipping user .agents/skills discovery: ${reason}`);
165
+ }
146
166
 
147
167
  // 4. Workspace skills (highest precedence)
148
168
  const projectSkills = await loadSkillsFromDir(
@@ -150,6 +170,11 @@ export class SkillManager {
150
170
  'project',
151
171
  );
152
172
  this.addSkillsWithPrecedence(projectSkills);
173
+ const projectAgentSkills = await loadSkillsFromDir(
174
+ storage.getProjectAgentSkillsDir(),
175
+ 'project',
176
+ );
177
+ this.addSkillsWithPrecedence(projectAgentSkills);
153
178
  }
154
179
 
155
180
  /**
@@ -9,6 +9,7 @@ import type {
9
9
  ToolCallConfirmationDetails,
10
10
  ToolInvocation,
11
11
  ToolResult,
12
+ LiveOutputUpdate,
12
13
  } from '@vybestack/llxprt-code-tools';
13
14
  import {
14
15
  BaseDeclarativeTool,
@@ -33,7 +34,7 @@ interface MockToolOptions {
33
34
  execute?: (
34
35
  params: { [key: string]: unknown },
35
36
  signal: AbortSignal,
36
- updateOutput?: (output: string) => void,
37
+ updateOutput?: (update: LiveOutputUpdate) => void,
37
38
  ) => Promise<ToolResult>;
38
39
  params?: object;
39
40
  messageBus?: IToolMessageBus;
@@ -64,7 +65,7 @@ class MockToolInvocation extends BaseToolInvocation<
64
65
 
65
66
  execute(
66
67
  signal: AbortSignal,
67
- updateOutput?: (output: string) => void,
68
+ updateOutput?: (update: LiveOutputUpdate) => void,
68
69
  ): Promise<ToolResult> {
69
70
  return this.tool.executeFn(this.params, signal, updateOutput);
70
71
  }
@@ -86,7 +87,7 @@ class MockToolInvocation extends BaseToolInvocation<
86
87
  type ExecuteFn = (
87
88
  params: { [key: string]: unknown },
88
89
  signal: AbortSignal,
89
- updateOutput?: (output: string) => void,
90
+ updateOutput?: (update: LiveOutputUpdate) => void,
90
91
  ) => Promise<ToolResult>;
91
92
 
92
93
  type ExecuteMock = ReturnType<typeof vi.fn<ExecuteFn>>;
@@ -12,6 +12,7 @@ import {
12
12
  type ToolCallConfirmationDetails,
13
13
  type ToolInvocation,
14
14
  type ToolResult,
15
+ type LiveOutputUpdate,
15
16
  Kind,
16
17
  } from '@vybestack/llxprt-code-tools';
17
18
  import { MessageBus } from '../confirmation-bus/message-bus.js';
@@ -61,7 +62,7 @@ async function executeMockTool(
61
62
  tool: { name: string; executeFn: ToolSpy },
62
63
  params: Record<string, unknown>,
63
64
  abortSignal: AbortSignal,
64
- updateOutput?: (output: string) => void,
65
+ updateOutput?: (update: LiveOutputUpdate) => void,
65
66
  ): Promise<ToolResult> {
66
67
  const result = await tool.executeFn(params, abortSignal, updateOutput);
67
68
  if (isToolResultLike(result)) {
@@ -87,7 +88,7 @@ class MockToolInvocation extends BaseToolInvocation<
87
88
 
88
89
  async execute(
89
90
  abortSignal: AbortSignal,
90
- updateOutput?: (output: string) => void,
91
+ updateOutput?: (update: LiveOutputUpdate) => void,
91
92
  ): Promise<ToolResult> {
92
93
  return executeMockTool(this.tool, this.params, abortSignal, updateOutput);
93
94
  }
@@ -168,7 +169,7 @@ export class MockModifiableToolInvocation extends BaseToolInvocation<
168
169
 
169
170
  async execute(
170
171
  abortSignal: AbortSignal,
171
- updateOutput?: (output: string) => void,
172
+ updateOutput?: (update: LiveOutputUpdate) => void,
172
173
  ): Promise<ToolResult> {
173
174
  return executeMockTool(this.tool, this.params, abortSignal, updateOutput);
174
175
  }
@@ -224,13 +224,7 @@ export class CoreShellToolHostAdapter implements IShellToolHost {
224
224
  private mapOutputEvent(event: ShellOutputEvent): ToolsShellOutputEvent {
225
225
  switch (event.type) {
226
226
  case 'data':
227
- return {
228
- type: 'data',
229
- chunk:
230
- typeof event.chunk === 'string'
231
- ? event.chunk
232
- : JSON.stringify(event.chunk),
233
- };
227
+ return { type: 'data', chunk: event.chunk };
234
228
  case 'binary_detected':
235
229
  return { type: 'binary_detected' };
236
230
  case 'binary_progress':