@jmoyers/harness 0.1.8 → 0.1.10
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/README.md +33 -155
- package/package.json +5 -1
- package/packages/harness-ai/src/anthropic-client.ts +99 -0
- package/packages/harness-ai/src/anthropic-protocol.ts +581 -0
- package/packages/harness-ai/src/anthropic-provider.ts +82 -0
- package/packages/harness-ai/src/async-iterable-stream.ts +65 -0
- package/packages/harness-ai/src/index.ts +36 -0
- package/packages/harness-ai/src/json-parse.ts +66 -0
- package/packages/harness-ai/src/sse.ts +80 -0
- package/packages/harness-ai/src/stream-object.ts +96 -0
- package/packages/harness-ai/src/stream-text.ts +1340 -0
- package/packages/harness-ai/src/types.ts +330 -0
- package/packages/harness-ai/src/ui-stream.ts +217 -0
- package/scripts/codex-live-mux-runtime.ts +123 -7
- package/scripts/control-plane-daemon.ts +20 -3
- package/scripts/harness.ts +566 -133
- package/src/cli/gateway-record.ts +16 -1
- package/src/control-plane/agent-realtime-api.ts +4 -0
- package/src/control-plane/prompt/agent-prompt-extractor.ts +191 -0
- package/src/control-plane/prompt/extractors/claude-prompt-extractor.ts +53 -0
- package/src/control-plane/prompt/extractors/codex-prompt-extractor.ts +50 -0
- package/src/control-plane/prompt/extractors/cursor-prompt-extractor.ts +56 -0
- package/src/control-plane/prompt/session-prompt-engine.ts +69 -0
- package/src/control-plane/prompt/thread-title-namer.ts +290 -0
- package/src/control-plane/stream-command-parser.ts +12 -0
- package/src/control-plane/stream-protocol.ts +109 -0
- package/src/control-plane/stream-server-command.ts +14 -0
- package/src/control-plane/stream-server-session-runtime.ts +12 -0
- package/src/control-plane/stream-server.ts +485 -19
- package/src/mux/input-shortcuts.ts +9 -0
- package/src/mux/live-mux/critique-review.ts +5 -1
- package/src/mux/live-mux/git-parsing.ts +24 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +8 -0
- package/src/mux/render-frame.ts +1 -1
- package/src/pty/pty_host.ts +46 -1
- package/src/services/control-plane.ts +22 -0
- package/src/services/runtime-control-actions.ts +69 -0
- package/src/services/runtime-navigation-input.ts +4 -0
- package/src/services/runtime-rail-input.ts +4 -0
- package/src/services/runtime-workspace-actions.ts +5 -0
- package/src/ui/global-shortcut-input.ts +2 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { createAnthropic, generateText } from '../../../packages/harness-ai/src/index.ts';
|
|
2
|
+
import type { StreamSessionPromptRecord } from '../stream-protocol.ts';
|
|
3
|
+
|
|
4
|
+
const THREAD_TITLE_ADAPTER_STATE_KEY = 'harnessThreadTitle';
|
|
5
|
+
const MAX_SANITIZED_PROMPT_CHARS = 1200;
|
|
6
|
+
const IMAGE_DATA_URL_PATTERN = /data:image\/[a-z0-9.+-]+;base64,[a-z0-9+/=\s]+/giu;
|
|
7
|
+
const MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*\]\([^)]*\)/gu;
|
|
8
|
+
const HTML_IMAGE_PATTERN = /<img\b[^>]*>/giu;
|
|
9
|
+
const LONG_BASE64_LINE_PATTERN = /^[A-Za-z0-9+/=\s]{160,}$/u;
|
|
10
|
+
const TITLE_WORD_PATTERN = /[A-Za-z0-9]+(?:'[A-Za-z0-9]+)*/g;
|
|
11
|
+
const TARGET_TITLE_WORD_COUNT = 2;
|
|
12
|
+
const FALLBACK_FILL_WORDS = ['current', 'thread'] as const;
|
|
13
|
+
const FALLBACK_STOP_WORDS = new Set([
|
|
14
|
+
'a',
|
|
15
|
+
'an',
|
|
16
|
+
'and',
|
|
17
|
+
'are',
|
|
18
|
+
'as',
|
|
19
|
+
'at',
|
|
20
|
+
'be',
|
|
21
|
+
'build',
|
|
22
|
+
'for',
|
|
23
|
+
'from',
|
|
24
|
+
'in',
|
|
25
|
+
'into',
|
|
26
|
+
'is',
|
|
27
|
+
'it',
|
|
28
|
+
'of',
|
|
29
|
+
'on',
|
|
30
|
+
'or',
|
|
31
|
+
'that',
|
|
32
|
+
'the',
|
|
33
|
+
'this',
|
|
34
|
+
'to',
|
|
35
|
+
'up',
|
|
36
|
+
'with',
|
|
37
|
+
]);
|
|
38
|
+
const DEFAULT_HAIKU_MODEL_ID = 'claude-3-5-haiku-latest';
|
|
39
|
+
|
|
40
|
+
interface ThreadTitlePromptHistoryEntry {
|
|
41
|
+
readonly text: string;
|
|
42
|
+
readonly observedAt: string;
|
|
43
|
+
readonly hash: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface ThreadTitleNamerInput {
|
|
47
|
+
readonly conversationId: string;
|
|
48
|
+
readonly agentType: string;
|
|
49
|
+
readonly currentTitle: string;
|
|
50
|
+
readonly promptHistory: readonly ThreadTitlePromptHistoryEntry[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ThreadTitleNamer {
|
|
54
|
+
suggest(input: ThreadTitleNamerInput): Promise<string | null>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface AnthropicThreadTitleNamerOptions {
|
|
58
|
+
readonly apiKey: string;
|
|
59
|
+
readonly modelId?: string;
|
|
60
|
+
readonly baseUrl?: string;
|
|
61
|
+
readonly fetch?: typeof fetch;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
65
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
return value as Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function trimmedString(value: unknown): string | null {
|
|
72
|
+
if (typeof value !== 'string') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const trimmed = value.trim();
|
|
76
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function historyEntriesFromAdapterState(
|
|
80
|
+
adapterState: Record<string, unknown>,
|
|
81
|
+
): readonly ThreadTitlePromptHistoryEntry[] {
|
|
82
|
+
const stored = asRecord(adapterState[THREAD_TITLE_ADAPTER_STATE_KEY]);
|
|
83
|
+
if (stored === null) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const promptsRaw = stored['prompts'];
|
|
87
|
+
if (!Array.isArray(promptsRaw)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const parsed: ThreadTitlePromptHistoryEntry[] = [];
|
|
91
|
+
for (const item of promptsRaw) {
|
|
92
|
+
const record = asRecord(item);
|
|
93
|
+
if (record === null) {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const text = trimmedString(record['text']);
|
|
97
|
+
const observedAt = trimmedString(record['observedAt']);
|
|
98
|
+
const hash = trimmedString(record['hash']);
|
|
99
|
+
if (text === null || observedAt === null || hash === null) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
parsed.push({
|
|
103
|
+
text,
|
|
104
|
+
observedAt,
|
|
105
|
+
hash,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return parsed;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function serializePromptHistory(
|
|
112
|
+
entries: readonly ThreadTitlePromptHistoryEntry[],
|
|
113
|
+
): Readonly<Record<string, unknown>> {
|
|
114
|
+
return {
|
|
115
|
+
prompts: entries.map((entry) => ({
|
|
116
|
+
text: entry.text,
|
|
117
|
+
observedAt: entry.observedAt,
|
|
118
|
+
hash: entry.hash,
|
|
119
|
+
})),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function sanitizePromptLine(line: string): string | null {
|
|
124
|
+
const withoutDataUrl = line
|
|
125
|
+
.replace(IMAGE_DATA_URL_PATTERN, ' ')
|
|
126
|
+
.replace(MARKDOWN_IMAGE_PATTERN, ' ')
|
|
127
|
+
.replace(HTML_IMAGE_PATTERN, ' ')
|
|
128
|
+
.trim();
|
|
129
|
+
if (withoutDataUrl.length === 0) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
if (LONG_BASE64_LINE_PATTERN.test(withoutDataUrl)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
return withoutDataUrl;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function sanitizePromptForThreadTitle(text: string): string | null {
|
|
139
|
+
const normalized = text
|
|
140
|
+
.replace(/\r\n/gu, '\n')
|
|
141
|
+
.replace(/\r/gu, '\n')
|
|
142
|
+
.split('\n')
|
|
143
|
+
.map((line) => sanitizePromptLine(line))
|
|
144
|
+
.filter((line): line is string => line !== null)
|
|
145
|
+
.join('\n')
|
|
146
|
+
.replace(/[ \t]{2,}/gu, ' ')
|
|
147
|
+
.trim();
|
|
148
|
+
if (normalized.length === 0) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
if (normalized.length <= MAX_SANITIZED_PROMPT_CHARS) {
|
|
152
|
+
return normalized;
|
|
153
|
+
}
|
|
154
|
+
return `${normalized.slice(0, MAX_SANITIZED_PROMPT_CHARS).trimEnd()}...`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function normalizeThreadTitleCandidate(value: string): string | null {
|
|
158
|
+
const compact = value.trim();
|
|
159
|
+
if (compact.length === 0) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const words = compact.match(TITLE_WORD_PATTERN)?.map((word) => word.trim()) ?? [];
|
|
163
|
+
if (words.length < TARGET_TITLE_WORD_COUNT) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const filtered = words
|
|
167
|
+
.map((word) => word.toLowerCase())
|
|
168
|
+
.filter((word) => word !== 'title' && word.length > 0);
|
|
169
|
+
if (filtered.length < TARGET_TITLE_WORD_COUNT) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
return filtered.slice(0, TARGET_TITLE_WORD_COUNT).join(' ');
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function fallbackThreadTitleFromPromptHistory(
|
|
176
|
+
promptHistory: readonly ThreadTitlePromptHistoryEntry[],
|
|
177
|
+
): string {
|
|
178
|
+
const selected: string[] = [];
|
|
179
|
+
for (let index = promptHistory.length - 1; index >= 0; index -= 1) {
|
|
180
|
+
const entry = promptHistory[index];
|
|
181
|
+
if (entry === undefined) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const words = entry.text.match(TITLE_WORD_PATTERN) ?? [];
|
|
185
|
+
for (const rawWord of words) {
|
|
186
|
+
const normalized = rawWord.toLowerCase();
|
|
187
|
+
if (normalized.length < 3 || FALLBACK_STOP_WORDS.has(normalized)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (selected.includes(normalized)) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
selected.push(normalized);
|
|
194
|
+
if (selected.length >= TARGET_TITLE_WORD_COUNT) {
|
|
195
|
+
return selected.join(' ');
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
for (const fallback of FALLBACK_FILL_WORDS) {
|
|
200
|
+
selected.push(fallback);
|
|
201
|
+
if (selected.length >= TARGET_TITLE_WORD_COUNT) {
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return selected.slice(0, TARGET_TITLE_WORD_COUNT).join(' ');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function readThreadTitlePromptHistory(
|
|
209
|
+
adapterState: Record<string, unknown>,
|
|
210
|
+
): readonly ThreadTitlePromptHistoryEntry[] {
|
|
211
|
+
return historyEntriesFromAdapterState(adapterState);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function appendThreadTitlePromptHistory(
|
|
215
|
+
adapterState: Record<string, unknown>,
|
|
216
|
+
prompt: StreamSessionPromptRecord,
|
|
217
|
+
): {
|
|
218
|
+
readonly nextAdapterState: Record<string, unknown>;
|
|
219
|
+
readonly promptHistory: readonly ThreadTitlePromptHistoryEntry[];
|
|
220
|
+
readonly added: boolean;
|
|
221
|
+
} {
|
|
222
|
+
const text = prompt.text === null ? null : sanitizePromptForThreadTitle(prompt.text);
|
|
223
|
+
const existing = historyEntriesFromAdapterState(adapterState);
|
|
224
|
+
if (text === null) {
|
|
225
|
+
return {
|
|
226
|
+
nextAdapterState: adapterState,
|
|
227
|
+
promptHistory: existing,
|
|
228
|
+
added: false,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const nextHistory: ThreadTitlePromptHistoryEntry[] = [
|
|
232
|
+
...existing,
|
|
233
|
+
{
|
|
234
|
+
text,
|
|
235
|
+
observedAt: prompt.observedAt,
|
|
236
|
+
hash: prompt.hash,
|
|
237
|
+
},
|
|
238
|
+
];
|
|
239
|
+
const nextAdapterState: Record<string, unknown> = {
|
|
240
|
+
...adapterState,
|
|
241
|
+
[THREAD_TITLE_ADAPTER_STATE_KEY]: serializePromptHistory(nextHistory),
|
|
242
|
+
};
|
|
243
|
+
return {
|
|
244
|
+
nextAdapterState,
|
|
245
|
+
promptHistory: nextHistory,
|
|
246
|
+
added: true,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createAnthropicThreadTitleNamer(
|
|
251
|
+
options: AnthropicThreadTitleNamerOptions,
|
|
252
|
+
): ThreadTitleNamer {
|
|
253
|
+
const anthropic = createAnthropic({
|
|
254
|
+
apiKey: options.apiKey,
|
|
255
|
+
...(options.baseUrl === undefined ? {} : { baseUrl: options.baseUrl }),
|
|
256
|
+
...(options.fetch === undefined ? {} : { fetch: options.fetch }),
|
|
257
|
+
});
|
|
258
|
+
const model = anthropic(options.modelId ?? DEFAULT_HAIKU_MODEL_ID);
|
|
259
|
+
return {
|
|
260
|
+
async suggest(input: ThreadTitleNamerInput): Promise<string | null> {
|
|
261
|
+
if (input.promptHistory.length === 0) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const promptLines = input.promptHistory.map(
|
|
265
|
+
(entry, index) => `${String(index + 1)}. ${entry.text}`,
|
|
266
|
+
);
|
|
267
|
+
const response = await generateText({
|
|
268
|
+
model,
|
|
269
|
+
system: [
|
|
270
|
+
'You name active coding-agent threads.',
|
|
271
|
+
'Use the full user prompt history to keep titles relevant and fresh.',
|
|
272
|
+
'Stay high-level and avoid low-level implementation details.',
|
|
273
|
+
'Return exactly 2 words in lowercase with no punctuation and no extra text.',
|
|
274
|
+
].join(' '),
|
|
275
|
+
prompt: [
|
|
276
|
+
`Agent: ${input.agentType}`,
|
|
277
|
+
`Current title: ${input.currentTitle}`,
|
|
278
|
+
`Conversation id: ${input.conversationId}`,
|
|
279
|
+
'Prompt history (oldest to newest):',
|
|
280
|
+
...promptLines,
|
|
281
|
+
'Return a new title now.',
|
|
282
|
+
].join('\n'),
|
|
283
|
+
maxOutputTokens: 16,
|
|
284
|
+
temperature: 0,
|
|
285
|
+
});
|
|
286
|
+
const normalized = normalizeThreadTitleCandidate(response.text);
|
|
287
|
+
return normalized ?? fallbackThreadTitleFromPromptHistory(input.promptHistory);
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -421,6 +421,17 @@ function parseConversationUpdate(record: CommandRecord): StreamCommand | null {
|
|
|
421
421
|
};
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
+
function parseConversationTitleRefresh(record: CommandRecord): StreamCommand | null {
|
|
425
|
+
const conversationId = readString(record['conversationId']);
|
|
426
|
+
if (conversationId === null) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
type: 'conversation.title.refresh',
|
|
431
|
+
conversationId,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
424
435
|
function parseConversationDelete(record: CommandRecord): StreamCommand | null {
|
|
425
436
|
const conversationId = readString(record['conversationId']);
|
|
426
437
|
if (conversationId === null) {
|
|
@@ -1628,6 +1639,7 @@ export const DEFAULT_STREAM_COMMAND_PARSERS: StreamCommandParserRegistry = {
|
|
|
1628
1639
|
'conversation.list': parseConversationList,
|
|
1629
1640
|
'conversation.archive': parseConversationArchive,
|
|
1630
1641
|
'conversation.update': parseConversationUpdate,
|
|
1642
|
+
'conversation.title.refresh': parseConversationTitleRefresh,
|
|
1631
1643
|
'conversation.delete': parseConversationDelete,
|
|
1632
1644
|
'repository.upsert': parseRepositoryUpsert,
|
|
1633
1645
|
'repository.get': parseRepositoryGet,
|
|
@@ -6,6 +6,8 @@ export type StreamSessionRuntimeStatus = 'running' | 'needs-input' | 'completed'
|
|
|
6
6
|
export type StreamSessionListSort = 'attention-first' | 'started-desc' | 'started-asc';
|
|
7
7
|
export type StreamTelemetrySource = 'otlp-log' | 'otlp-metric' | 'otlp-trace' | 'history';
|
|
8
8
|
export type StreamTelemetryStatusHint = 'running' | 'completed' | 'needs-input';
|
|
9
|
+
export type StreamPromptCaptureSource = 'otlp-log' | 'hook-notify' | 'history';
|
|
10
|
+
export type StreamPromptConfidence = 'high' | 'medium' | 'low';
|
|
9
11
|
export type StreamSessionControllerType = 'human' | 'agent' | 'automation';
|
|
10
12
|
export type StreamSessionDisplayPhase = 'needs-action' | 'starting' | 'working' | 'idle' | 'exited';
|
|
11
13
|
|
|
@@ -46,6 +48,16 @@ export interface StreamSessionKeyEventRecord {
|
|
|
46
48
|
statusHint: StreamTelemetryStatusHint | null;
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
export interface StreamSessionPromptRecord {
|
|
52
|
+
text: string | null;
|
|
53
|
+
hash: string;
|
|
54
|
+
confidence: StreamPromptConfidence;
|
|
55
|
+
captureSource: StreamPromptCaptureSource;
|
|
56
|
+
providerEventName: string | null;
|
|
57
|
+
providerPayloadKeys: string[];
|
|
58
|
+
observedAt: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
49
61
|
interface DirectoryUpsertCommand {
|
|
50
62
|
type: 'directory.upsert';
|
|
51
63
|
directoryId?: string;
|
|
@@ -107,6 +119,11 @@ interface ConversationUpdateCommand {
|
|
|
107
119
|
title: string;
|
|
108
120
|
}
|
|
109
121
|
|
|
122
|
+
interface ConversationTitleRefreshCommand {
|
|
123
|
+
type: 'conversation.title.refresh';
|
|
124
|
+
conversationId: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
110
127
|
interface ConversationDeleteCommand {
|
|
111
128
|
type: 'conversation.delete';
|
|
112
129
|
conversationId: string;
|
|
@@ -482,6 +499,7 @@ export type StreamCommand =
|
|
|
482
499
|
| ConversationListCommand
|
|
483
500
|
| ConversationArchiveCommand
|
|
484
501
|
| ConversationUpdateCommand
|
|
502
|
+
| ConversationTitleRefreshCommand
|
|
485
503
|
| ConversationDeleteCommand
|
|
486
504
|
| RepositoryUpsertCommand
|
|
487
505
|
| RepositoryGetCommand
|
|
@@ -708,6 +726,14 @@ export type StreamObservedEvent =
|
|
|
708
726
|
directoryId: string | null;
|
|
709
727
|
conversationId: string | null;
|
|
710
728
|
}
|
|
729
|
+
| {
|
|
730
|
+
type: 'session-prompt-event';
|
|
731
|
+
sessionId: string;
|
|
732
|
+
prompt: StreamSessionPromptRecord;
|
|
733
|
+
ts: string;
|
|
734
|
+
directoryId: string | null;
|
|
735
|
+
conversationId: string | null;
|
|
736
|
+
}
|
|
711
737
|
| {
|
|
712
738
|
type: 'session-control';
|
|
713
739
|
sessionId: string;
|
|
@@ -1057,6 +1083,20 @@ function parseTelemetryStatusHint(value: unknown): StreamTelemetryStatusHint | n
|
|
|
1057
1083
|
return undefined;
|
|
1058
1084
|
}
|
|
1059
1085
|
|
|
1086
|
+
function parsePromptCaptureSource(value: unknown): StreamPromptCaptureSource | null {
|
|
1087
|
+
if (value === 'otlp-log' || value === 'hook-notify' || value === 'history') {
|
|
1088
|
+
return value;
|
|
1089
|
+
}
|
|
1090
|
+
return null;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function parsePromptConfidence(value: unknown): StreamPromptConfidence | null {
|
|
1094
|
+
if (value === 'high' || value === 'medium' || value === 'low') {
|
|
1095
|
+
return value;
|
|
1096
|
+
}
|
|
1097
|
+
return null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1060
1100
|
function parseTelemetrySummary(value: unknown): StreamTelemetrySummary | null | undefined {
|
|
1061
1101
|
if (value === undefined) {
|
|
1062
1102
|
return undefined;
|
|
@@ -1189,6 +1229,50 @@ function parseSessionKeyEventRecord(value: unknown): StreamSessionKeyEventRecord
|
|
|
1189
1229
|
};
|
|
1190
1230
|
}
|
|
1191
1231
|
|
|
1232
|
+
function parseSessionPromptRecord(value: unknown): StreamSessionPromptRecord | null {
|
|
1233
|
+
const record = asRecord(value);
|
|
1234
|
+
if (record === null) {
|
|
1235
|
+
return null;
|
|
1236
|
+
}
|
|
1237
|
+
const text = record['text'] === null ? null : readString(record['text']);
|
|
1238
|
+
const hash = readString(record['hash']);
|
|
1239
|
+
const confidence = parsePromptConfidence(record['confidence']);
|
|
1240
|
+
const captureSource = parsePromptCaptureSource(record['captureSource']);
|
|
1241
|
+
const providerEventName =
|
|
1242
|
+
record['providerEventName'] === null ? null : readString(record['providerEventName']);
|
|
1243
|
+
const providerPayloadKeysValue = record['providerPayloadKeys'];
|
|
1244
|
+
const observedAt = readString(record['observedAt']);
|
|
1245
|
+
if (
|
|
1246
|
+
(text === null && record['text'] !== null) ||
|
|
1247
|
+
hash === null ||
|
|
1248
|
+
hash.trim().length === 0 ||
|
|
1249
|
+
confidence === null ||
|
|
1250
|
+
captureSource === null ||
|
|
1251
|
+
(providerEventName === null && record['providerEventName'] !== null) ||
|
|
1252
|
+
!Array.isArray(providerPayloadKeysValue) ||
|
|
1253
|
+
observedAt === null
|
|
1254
|
+
) {
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
const providerPayloadKeys: string[] = [];
|
|
1258
|
+
for (const entry of providerPayloadKeysValue) {
|
|
1259
|
+
const key = readString(entry);
|
|
1260
|
+
if (key === null) {
|
|
1261
|
+
return null;
|
|
1262
|
+
}
|
|
1263
|
+
providerPayloadKeys.push(key);
|
|
1264
|
+
}
|
|
1265
|
+
return {
|
|
1266
|
+
text: record['text'] === null ? null : text,
|
|
1267
|
+
hash,
|
|
1268
|
+
confidence,
|
|
1269
|
+
captureSource,
|
|
1270
|
+
providerEventName: record['providerEventName'] === null ? null : providerEventName,
|
|
1271
|
+
providerPayloadKeys,
|
|
1272
|
+
observedAt,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1192
1276
|
function parseStreamObservedEvent(value: unknown): StreamObservedEvent | null {
|
|
1193
1277
|
const record = asRecord(value);
|
|
1194
1278
|
if (record === null) {
|
|
@@ -1609,6 +1693,31 @@ function parseStreamObservedEvent(value: unknown): StreamObservedEvent | null {
|
|
|
1609
1693
|
};
|
|
1610
1694
|
}
|
|
1611
1695
|
|
|
1696
|
+
if (type === 'session-prompt-event') {
|
|
1697
|
+
const sessionId = readString(record['sessionId']);
|
|
1698
|
+
const prompt = parseSessionPromptRecord(record['prompt']);
|
|
1699
|
+
const ts = readString(record['ts']);
|
|
1700
|
+
const directoryId = readString(record['directoryId']);
|
|
1701
|
+
const conversationId = readString(record['conversationId']);
|
|
1702
|
+
if (
|
|
1703
|
+
sessionId === null ||
|
|
1704
|
+
prompt === null ||
|
|
1705
|
+
ts === null ||
|
|
1706
|
+
(record['directoryId'] !== null && directoryId === null) ||
|
|
1707
|
+
(record['conversationId'] !== null && conversationId === null)
|
|
1708
|
+
) {
|
|
1709
|
+
return null;
|
|
1710
|
+
}
|
|
1711
|
+
return {
|
|
1712
|
+
type,
|
|
1713
|
+
sessionId,
|
|
1714
|
+
prompt,
|
|
1715
|
+
ts,
|
|
1716
|
+
directoryId: record['directoryId'] === null ? null : directoryId,
|
|
1717
|
+
conversationId: record['conversationId'] === null ? null : conversationId,
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1612
1721
|
if (type === 'session-control') {
|
|
1613
1722
|
const sessionId = readString(record['sessionId']);
|
|
1614
1723
|
const action = readString(record['action']);
|
|
@@ -419,6 +419,11 @@ interface ExecuteCommandContext {
|
|
|
419
419
|
}>;
|
|
420
420
|
};
|
|
421
421
|
readonly streamCursor: number;
|
|
422
|
+
refreshConversationTitle(conversationId: string): Promise<{
|
|
423
|
+
conversation: ControlPlaneConversationRecord;
|
|
424
|
+
status: 'updated' | 'unchanged' | 'skipped';
|
|
425
|
+
reason: string | null;
|
|
426
|
+
}>;
|
|
422
427
|
refreshGitStatusForDirectory(
|
|
423
428
|
directory: ControlPlaneDirectoryRecord,
|
|
424
429
|
options?: {
|
|
@@ -1403,6 +1408,15 @@ export async function executeStreamServerCommand(
|
|
|
1403
1408
|
};
|
|
1404
1409
|
}
|
|
1405
1410
|
|
|
1411
|
+
if (command.type === 'conversation.title.refresh') {
|
|
1412
|
+
const refreshed = await ctx.refreshConversationTitle(command.conversationId);
|
|
1413
|
+
return {
|
|
1414
|
+
conversation: ctx.conversationRecord(refreshed.conversation),
|
|
1415
|
+
status: refreshed.status,
|
|
1416
|
+
reason: refreshed.reason,
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1406
1420
|
if (command.type === 'conversation.delete') {
|
|
1407
1421
|
const existing = ctx.stateStore.getConversation(command.conversationId);
|
|
1408
1422
|
if (existing === null) {
|
|
@@ -5,10 +5,12 @@ import type {
|
|
|
5
5
|
StreamSessionController,
|
|
6
6
|
StreamSessionEvent,
|
|
7
7
|
StreamSessionKeyEventRecord,
|
|
8
|
+
StreamSessionPromptRecord,
|
|
8
9
|
StreamSessionRuntimeStatus,
|
|
9
10
|
StreamSessionStatusModel,
|
|
10
11
|
StreamSignal,
|
|
11
12
|
} from './stream-protocol.ts';
|
|
13
|
+
import { SessionPromptEngine } from './prompt/session-prompt-engine.ts';
|
|
12
14
|
|
|
13
15
|
const CLAUDE_NEEDS_INPUT_NOTIFICATION_TYPES = new Set([
|
|
14
16
|
'permissionrequest',
|
|
@@ -22,6 +24,7 @@ const CLAUDE_RUNNING_NOTIFICATION_TYPES = new Set([
|
|
|
22
24
|
'approvalapproved',
|
|
23
25
|
'approvalgranted',
|
|
24
26
|
]);
|
|
27
|
+
const sessionPromptEngine = new SessionPromptEngine();
|
|
25
28
|
|
|
26
29
|
interface RuntimeSession {
|
|
27
30
|
id: string;
|
|
@@ -83,6 +86,7 @@ interface StreamRuntimeContext {
|
|
|
83
86
|
state: RuntimeSession,
|
|
84
87
|
keyEvent: StreamSessionKeyEventRecord,
|
|
85
88
|
): void;
|
|
89
|
+
publishSessionPromptObservedEvent(state: RuntimeSession, prompt: StreamSessionPromptRecord): void;
|
|
86
90
|
refreshSessionStatusModel(state: RuntimeSession, observedAt: string): void;
|
|
87
91
|
toPublicSessionController(
|
|
88
92
|
controller: StreamSessionController | null,
|
|
@@ -557,6 +561,14 @@ export function handleSessionEvent(
|
|
|
557
561
|
ctx.stateStore.updateConversationAdapterState(sessionState.id, mergedAdapterState);
|
|
558
562
|
}
|
|
559
563
|
if (mapped.type === 'notify') {
|
|
564
|
+
const promptEvent = sessionPromptEngine.extractFromNotify({
|
|
565
|
+
agentType: sessionState.agentType,
|
|
566
|
+
payload: mapped.record.payload,
|
|
567
|
+
observedAt,
|
|
568
|
+
});
|
|
569
|
+
if (promptEvent !== null) {
|
|
570
|
+
ctx.publishSessionPromptObservedEvent(sessionState, promptEvent);
|
|
571
|
+
}
|
|
560
572
|
const keyEvent =
|
|
561
573
|
notifyKeyEventFromPayload(sessionState.agentType, mapped.record.payload, observedAt) ??
|
|
562
574
|
unmappedNotifyKeyEventFromPayload(
|