@modelprofile.com/flexharness 10.0.0 → 10.1.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.
Files changed (42) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/classes.stores.js +11 -119
  3. package/dist_ts/errors.d.ts +7 -2
  4. package/dist_ts/errors.js +1 -1
  5. package/dist_ts/utils.storerules.d.ts +28 -0
  6. package/dist_ts/utils.storerules.js +107 -0
  7. package/dist_ts_chat_harness/classes.transcript.d.ts +88 -0
  8. package/dist_ts_chat_harness/classes.transcript.js +268 -0
  9. package/dist_ts_chat_harness/functions.intents.d.ts +62 -0
  10. package/dist_ts_chat_harness/functions.intents.js +96 -0
  11. package/dist_ts_chat_harness/functions.mapping.d.ts +33 -0
  12. package/dist_ts_chat_harness/functions.mapping.js +220 -0
  13. package/dist_ts_chat_harness/index.d.ts +3 -0
  14. package/dist_ts_chat_harness/index.js +4 -0
  15. package/dist_ts_chat_harness/plugins.d.ts +1 -0
  16. package/dist_ts_chat_harness/plugins.js +2 -0
  17. package/dist_ts_stores_nosqldb/classes.nosqldbstores.d.ts +31 -0
  18. package/dist_ts_stores_nosqldb/classes.nosqldbstores.js +312 -0
  19. package/dist_ts_stores_nosqldb/classes.recordstore.d.ts +110 -0
  20. package/dist_ts_stores_nosqldb/classes.recordstore.js +437 -0
  21. package/dist_ts_stores_nosqldb/index.d.ts +2 -0
  22. package/dist_ts_stores_nosqldb/index.js +3 -0
  23. package/dist_ts_stores_nosqldb/plugins.d.ts +7 -0
  24. package/dist_ts_stores_nosqldb/plugins.js +10 -0
  25. package/package.json +24 -4
  26. package/readme.md +4 -0
  27. package/ts/00_commitinfo_data.ts +1 -1
  28. package/ts/classes.stores.ts +28 -172
  29. package/ts/errors.ts +8 -2
  30. package/ts/readme.md +4 -0
  31. package/ts/utils.storerules.ts +177 -0
  32. package/ts_chat_harness/classes.transcript.ts +365 -0
  33. package/ts_chat_harness/functions.intents.ts +169 -0
  34. package/ts_chat_harness/functions.mapping.ts +267 -0
  35. package/ts_chat_harness/index.ts +33 -0
  36. package/ts_chat_harness/plugins.ts +18 -0
  37. package/ts_chat_harness/readme.md +114 -0
  38. package/ts_stores_nosqldb/classes.nosqldbstores.ts +465 -0
  39. package/ts_stores_nosqldb/classes.recordstore.ts +560 -0
  40. package/ts_stores_nosqldb/index.ts +9 -0
  41. package/ts_stores_nosqldb/plugins.ts +38 -0
  42. package/ts_stores_nosqldb/readme.md +53 -0
@@ -0,0 +1,365 @@
1
+ import type * as plugins from './plugins.js';
2
+ import type {
3
+ IFlexMessage,
4
+ IFlexPermissionRequest,
5
+ IFlexProjectStateResult,
6
+ IFlexSession,
7
+ TFlexHarnessEvent,
8
+ TFlexMessagePart,
9
+ } from '../ts/interfaces.js';
10
+ import {
11
+ flexMessageToHarnessRows,
12
+ flexPartRowId,
13
+ flexPartTimingKey,
14
+ flexPermissionToHarness,
15
+ flexProjectToHarnessScratchpad,
16
+ flexProjectToHarnessTodos,
17
+ flexSessionToHarnessStatus,
18
+ flexToolPartToHarness,
19
+ flexUsageToHarness,
20
+ type IFlexPartTiming,
21
+ } from './functions.mapping.js';
22
+
23
+ export interface IFlexHarnessTranscriptOptions {
24
+ scopeId: string;
25
+ sessionId: string;
26
+ }
27
+
28
+ /** What `load()` reads: the session's authoritative state, as the harness APIs return it. */
29
+ export interface IFlexHarnessTranscriptSnapshot {
30
+ /** `getMessages()` of the session. */
31
+ messages: readonly IFlexMessage[];
32
+ /** `listPendingPermissions()` of the session. */
33
+ permissions?: readonly IFlexPermissionRequest[];
34
+ /** `getSession()`; states whether a run is active or failed. */
35
+ session?: IFlexSession;
36
+ /** `getProjectState()`, when project management is enabled. */
37
+ project?: IFlexProjectStateResult;
38
+ }
39
+
40
+ /** The inputs of `dees-harness-chat`, as plain data the transcript does not share. */
41
+ export interface IFlexHarnessTranscriptView {
42
+ messages: plugins.IHarnessMessage[];
43
+ permissions: plugins.IHarnessPermissionRequest[];
44
+ /** FlexHarness asks no questions; always empty. */
45
+ questions: plugins.IHarnessQuestionRequest[];
46
+ status: plugins.IHarnessStatus;
47
+ usage: plugins.IHarnessUsage;
48
+ todos: plugins.IHarnessTodoItem[];
49
+ scratchpad?: plugins.IHarnessScratchpad;
50
+ }
51
+
52
+ /** Where an event stands in its harness instance's event stream, as the host received it. */
53
+ export interface IFlexHarnessEventDelivery {
54
+ /**
55
+ * The `sequence` of the event of the same harness instance the host received just before this
56
+ * one, of any session; `undefined` for the first event after `load()`. The host forwards every
57
+ * event of the instance in order, and calls `load()` after a gap or a reconnect.
58
+ */
59
+ precedingSequence: number | undefined;
60
+ }
61
+
62
+ /**
63
+ * What one event changed. Apply `deltas` with `applyDelta()` in order; when `messages` is true,
64
+ * assign `transcript.view().messages` instead, which already holds the deltas. `reload` means
65
+ * the transcript can no longer follow the session incrementally: read the session again and
66
+ * pass it to `load()`.
67
+ */
68
+ export interface IFlexHarnessTranscriptChange {
69
+ messages: boolean;
70
+ deltas: plugins.THarnessStreamDelta[];
71
+ permissions: boolean;
72
+ status: boolean;
73
+ usage: boolean;
74
+ reload: boolean;
75
+ }
76
+
77
+ const utf8 = new TextEncoder();
78
+
79
+ /** UTF-8 bytes `delta` adds to `text`, counted the way FlexHarness counts them. */
80
+ function appendedUtf8Bytes(text: string, delta: string): number {
81
+ const trailing = text.charCodeAt(text.length - 1);
82
+ const leading = delta.charCodeAt(0);
83
+ const joinsSurrogatePair = trailing >= 0xd800 && trailing <= 0xdbff
84
+ && leading >= 0xdc00 && leading <= 0xdfff;
85
+ return utf8.encode(delta).length - (joinsSurrogatePair ? 2 : 0);
86
+ }
87
+
88
+ function sumUsage(
89
+ total: plugins.IHarnessUsage,
90
+ usage: plugins.IHarnessUsage,
91
+ ): plugins.IHarnessUsage {
92
+ const sum = (key: keyof plugins.IHarnessUsage) => (total[key] ?? 0) + (usage[key] ?? 0);
93
+ return {
94
+ inputTokens: sum('inputTokens'),
95
+ outputTokens: sum('outputTokens'),
96
+ totalTokens: sum('totalTokens'),
97
+ cacheReadTokens: sum('cacheReadTokens'),
98
+ cacheWriteTokens: sum('cacheWriteTokens'),
99
+ };
100
+ }
101
+
102
+ const noChange = (): IFlexHarnessTranscriptChange => ({
103
+ messages: false,
104
+ deltas: [],
105
+ permissions: false,
106
+ status: false,
107
+ usage: false,
108
+ reload: false,
109
+ });
110
+
111
+ /**
112
+ * Follows one FlexHarness session and keeps the `dees-harness-chat` inputs for it. It is a pure
113
+ * reducer over the session's public messages and events: it imports no runtime code, so it runs
114
+ * in the browser beside the components as well as on the server beside the harness.
115
+ */
116
+ export class FlexHarnessTranscript {
117
+ public readonly scopeId: string;
118
+ public readonly sessionId: string;
119
+ private flexMessages: IFlexMessage[] = [];
120
+ private readonly timings = new Map<string, IFlexPartTiming>();
121
+ private readonly partBytes = new Map<string, number>();
122
+ private readonly permissionRows = new Map<string, plugins.IHarnessPermissionRequest>();
123
+ private status: plugins.IHarnessStatus = { type: 'idle' };
124
+ private usage: plugins.IHarnessUsage = {};
125
+ private todos: plugins.IHarnessTodoItem[] = [];
126
+ private scratchpad?: plugins.IHarnessScratchpad;
127
+ private runningQueueId?: string;
128
+
129
+ constructor(options: IFlexHarnessTranscriptOptions) {
130
+ this.scopeId = options.scopeId;
131
+ this.sessionId = options.sessionId;
132
+ }
133
+
134
+ /** The queue entry whose run is running; a steer joins it. */
135
+ public get activeQueueId(): string | undefined {
136
+ return this.runningQueueId;
137
+ }
138
+
139
+ /** Replaces everything with the session's authoritative state. */
140
+ public load(snapshot: IFlexHarnessTranscriptSnapshot): void {
141
+ this.flexMessages = [];
142
+ this.todos = [];
143
+ this.scratchpad = undefined;
144
+ this.runningQueueId = undefined;
145
+ this.timings.clear();
146
+ this.partBytes.clear();
147
+ for (const message of snapshot.messages) this.upsertMessage(message);
148
+ this.permissionRows.clear();
149
+ for (const request of snapshot.permissions ?? []) {
150
+ this.permissionRows.set(request.permissionId, flexPermissionToHarness(request));
151
+ }
152
+ this.status = snapshot.session ? flexSessionToHarnessStatus(snapshot.session) : { type: 'idle' };
153
+ this.usage = this.flexMessages.reduce<plugins.IHarnessUsage>(
154
+ (total, message) => message.usage ? sumUsage(total, flexUsageToHarness(message.usage)) : total,
155
+ {},
156
+ );
157
+ if (snapshot.project) this.applyProjectState(snapshot.project);
158
+ }
159
+
160
+ /** Takes in the session's task list and scratchpad after `getProjectState()`. */
161
+ public applyProjectState(project: IFlexProjectStateResult): void {
162
+ this.todos = flexProjectToHarnessTodos(project);
163
+ this.scratchpad = flexProjectToHarnessScratchpad(this.sessionId, project);
164
+ }
165
+
166
+ /** A copy of the component inputs; the transcript keeps its own objects. */
167
+ public view(): IFlexHarnessTranscriptView {
168
+ return structuredClone({
169
+ messages: this.flexMessages.flatMap((message, index) =>
170
+ flexMessageToHarnessRows(message, index, this.timings)),
171
+ permissions: [...this.permissionRows.values()],
172
+ questions: [],
173
+ status: this.status,
174
+ usage: this.usage,
175
+ todos: this.todos,
176
+ ...(this.scratchpad === undefined ? {} : { scratchpad: this.scratchpad }),
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Takes one event of the harness instance. An event that does not directly follow
182
+ * `delivery.precedingSequence` means events were lost, possibly of this session, so the change
183
+ * asks for a reload.
184
+ */
185
+ public applyEvent(
186
+ event: TFlexHarnessEvent,
187
+ delivery: IFlexHarnessEventDelivery,
188
+ ): IFlexHarnessTranscriptChange {
189
+ const change = noChange();
190
+ if (
191
+ delivery.precedingSequence !== undefined
192
+ && event.sequence !== delivery.precedingSequence + 1
193
+ ) {
194
+ change.reload = true;
195
+ return change;
196
+ }
197
+ if (event.scopeId !== this.scopeId || event.sessionId !== this.sessionId) return change;
198
+ const timestamp = Date.parse(event.timestamp);
199
+ switch (event.type) {
200
+ case 'session.created':
201
+ case 'session.updated':
202
+ this.setStatus(change, flexSessionToHarnessStatus(event.session));
203
+ break;
204
+ case 'session.deleted':
205
+ this.load({ messages: [] });
206
+ change.messages = change.permissions = change.status = change.usage = true;
207
+ break;
208
+ case 'run.started':
209
+ this.setStatus(change, { type: 'busy' });
210
+ break;
211
+ case 'message.created':
212
+ case 'message.updated':
213
+ this.upsertMessage(event.message);
214
+ change.messages = true;
215
+ break;
216
+ case 'part.started':
217
+ this.timings.set(flexPartTimingKey(event.messageId, event.partId), { startedAt: timestamp });
218
+ this.applyPartSnapshot(change, event.messageId, event.partIndex, event.part);
219
+ break;
220
+ case 'part.updated':
221
+ this.applyPartSnapshot(change, event.messageId, event.partIndex, event.part);
222
+ break;
223
+ case 'part.completed': {
224
+ const key = flexPartTimingKey(event.messageId, event.partId);
225
+ this.timings.set(key, { ...this.timings.get(key), endedAt: timestamp });
226
+ this.applyPartSnapshot(change, event.messageId, event.partIndex, event.part);
227
+ break;
228
+ }
229
+ case 'part.delta':
230
+ this.applyPartDelta(change, event);
231
+ break;
232
+ case 'permission.requested':
233
+ this.permissionRows.set(event.request.permissionId, flexPermissionToHarness(event.request));
234
+ change.permissions = true;
235
+ break;
236
+ case 'permission.resolved': {
237
+ const row = this.permissionRows.get(event.request.permissionId);
238
+ if (!row) break;
239
+ row.response = event.decision;
240
+ row.respondedAt = timestamp;
241
+ change.permissions = true;
242
+ break;
243
+ }
244
+ case 'run.finished':
245
+ this.upsertMessage(event.message);
246
+ change.messages = true;
247
+ if (event.message.usage) {
248
+ this.usage = sumUsage(this.usage, flexUsageToHarness(event.message.usage));
249
+ change.usage = true;
250
+ }
251
+ this.setStatus(change, event.status === 'failed'
252
+ ? { type: 'error', ...(event.error ? { message: event.error.message } : {}) }
253
+ : { type: 'idle' });
254
+ break;
255
+ case 'prompt.started':
256
+ case 'prompt.running':
257
+ this.runningQueueId = event.queueId;
258
+ break;
259
+ case 'prompt.finished':
260
+ if (this.runningQueueId === event.queueId) this.runningQueueId = undefined;
261
+ break;
262
+ case 'session.history.changed':
263
+ change.reload = true;
264
+ break;
265
+ default:
266
+ break;
267
+ }
268
+ return change;
269
+ }
270
+
271
+ private setStatus(change: IFlexHarnessTranscriptChange, status: plugins.IHarnessStatus): void {
272
+ if (status.type === this.status.type && status.message === this.status.message) return;
273
+ this.status = status;
274
+ change.status = true;
275
+ }
276
+
277
+ private upsertMessage(message: IFlexMessage): void {
278
+ const index = this.flexMessages.findIndex((entry) => entry.messageId === message.messageId);
279
+ const copy = structuredClone(message);
280
+ if (index === -1) this.flexMessages.push(copy);
281
+ else this.flexMessages[index] = copy;
282
+ for (const part of copy.parts) {
283
+ if (part.type === 'text' || part.type === 'reasoning') {
284
+ this.partBytes.set(flexPartTimingKey(copy.messageId, part.partId), utf8.encode(part.text).length);
285
+ }
286
+ }
287
+ }
288
+
289
+ private applyPartSnapshot(
290
+ change: IFlexHarnessTranscriptChange,
291
+ messageId: string,
292
+ partIndex: number,
293
+ part: TFlexMessagePart,
294
+ ): void {
295
+ const message = this.flexMessages.find((entry) => entry.messageId === messageId);
296
+ if (!message || partIndex > message.parts.length) {
297
+ change.reload = true;
298
+ return;
299
+ }
300
+ const previous = message.parts[partIndex];
301
+ message.parts[partIndex] = structuredClone(part);
302
+ const key = flexPartTimingKey(messageId, part.partId);
303
+ if (part.type === 'text' || part.type === 'reasoning') {
304
+ this.partBytes.set(key, utf8.encode(part.text).length);
305
+ }
306
+ const rowId = flexPartRowId(messageId, part.partId);
307
+ if (previous?.partId !== part.partId || previous.type !== part.type) {
308
+ change.messages = true;
309
+ return;
310
+ }
311
+ if (part.type === 'tool') {
312
+ change.deltas.push({
313
+ type: 'tool-update',
314
+ messageId: rowId,
315
+ tool: flexToolPartToHarness(part, this.timings.get(key)),
316
+ });
317
+ } else if (part.type === 'text' && previous.type === 'text') {
318
+ if (previous.text !== part.text) change.messages = true;
319
+ else if (message.status !== 'streaming' || partIndex !== message.parts.length - 1) {
320
+ change.deltas.push({ type: 'message-end', messageId: rowId });
321
+ }
322
+ } else if (part.type === 'reasoning' && previous.type === 'reasoning') {
323
+ if (previous.text !== part.text) change.messages = true;
324
+ else if (part.status !== 'running') {
325
+ change.deltas.push({
326
+ type: 'reasoning-end',
327
+ messageId: rowId,
328
+ partId: part.partId,
329
+ endedAt: this.timings.get(key)?.endedAt,
330
+ });
331
+ }
332
+ } else {
333
+ change.messages = true;
334
+ }
335
+ }
336
+
337
+ private applyPartDelta(
338
+ change: IFlexHarnessTranscriptChange,
339
+ event: Extract<TFlexHarnessEvent, { type: 'part.delta' }>,
340
+ ): void {
341
+ const message = this.flexMessages.find((entry) => entry.messageId === event.messageId);
342
+ const part = message?.parts[event.partIndex];
343
+ const key = flexPartTimingKey(event.messageId, event.partId);
344
+ if (
345
+ !part
346
+ || part.partId !== event.partId
347
+ || part.type !== event.partType
348
+ || this.partBytes.get(key) !== event.baseTextUtf8Bytes
349
+ ) {
350
+ change.reload = true;
351
+ return;
352
+ }
353
+ const bytes = event.baseTextUtf8Bytes + appendedUtf8Bytes(part.text, event.delta);
354
+ if (bytes !== event.textUtf8Bytes) {
355
+ change.reload = true;
356
+ return;
357
+ }
358
+ part.text += event.delta;
359
+ this.partBytes.set(key, bytes);
360
+ const rowId = flexPartRowId(event.messageId, event.partId);
361
+ change.deltas.push(event.partType === 'text'
362
+ ? { type: 'text', messageId: rowId, delta: event.delta }
363
+ : { type: 'reasoning', messageId: rowId, partId: event.partId, delta: event.delta });
364
+ }
365
+ }
@@ -0,0 +1,169 @@
1
+ import type * as plugins from './plugins.js';
2
+ import type { FlexHarness } from '../ts/classes.flexharness.js';
3
+ import type {
4
+ IFlexPromptOptions,
5
+ IFlexPromptQueueAdmission,
6
+ TFlexPermissionDecision,
7
+ TFlexPromptPart,
8
+ } from '../ts/interfaces.js';
9
+
10
+ /** The session a chat view shows. */
11
+ export interface IFlexHarnessChatTarget {
12
+ scopeId: string;
13
+ sessionId: string;
14
+ }
15
+
16
+ /**
17
+ * One call a chat view asks the harness to make, as plain data a host can carry from the browser
18
+ * to the server that owns the harness.
19
+ */
20
+ export type TFlexHarnessChatIntent =
21
+ | (IFlexHarnessChatTarget & {
22
+ type: 'enqueuePrompt';
23
+ prompt: TFlexPromptPart[];
24
+ options: IFlexPromptOptions;
25
+ })
26
+ | (IFlexHarnessChatTarget & {
27
+ type: 'steerPrompt';
28
+ queueId: string;
29
+ steerId: string;
30
+ prompt: TFlexPromptPart[];
31
+ options: IFlexPromptOptions;
32
+ })
33
+ | (IFlexHarnessChatTarget & {
34
+ type: 'respondToPermission';
35
+ permissionId: string;
36
+ decision: TFlexPermissionDecision;
37
+ })
38
+ | (IFlexHarnessChatTarget & { type: 'abort' });
39
+
40
+ export interface IFlexHarnessSendIntentOptions {
41
+ /** The running queue entry, `FlexHarnessTranscript.activeQueueId`; a steer joins it. */
42
+ activeQueueId?: string;
43
+ /**
44
+ * The model hint the prompt carries. Defaults to the composer's `model`. A host that offers
45
+ * accounts or reasoning efforts encodes them in its own hint scheme here.
46
+ */
47
+ modelHint?: string;
48
+ }
49
+
50
+ function base64Utf8(text: string): string {
51
+ const bytes = new TextEncoder().encode(text);
52
+ let binary = '';
53
+ for (let offset = 0; offset < bytes.length; offset += 0x8000) {
54
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 0x8000));
55
+ }
56
+ return btoa(binary);
57
+ }
58
+
59
+ function attachmentToPromptPart(attachment: plugins.IHarnessAttachment): TFlexPromptPart {
60
+ if (attachment.truncated) {
61
+ throw new Error(`Attachment "${attachment.name}" is truncated and cannot be sent.`);
62
+ }
63
+ const described = { mediaType: attachment.mediaType, name: attachment.name };
64
+ if (attachment.kind === 'text' && attachment.text !== undefined) {
65
+ return { type: 'file', data: base64Utf8(attachment.text), ...described };
66
+ }
67
+ if (attachment.dataBase64 === undefined) {
68
+ throw new Error(`Attachment "${attachment.name}" carries no payload.`);
69
+ }
70
+ return attachment.kind === 'image'
71
+ ? { type: 'image', data: attachment.dataBase64, ...described }
72
+ : { type: 'file', data: attachment.dataBase64, ...described };
73
+ }
74
+
75
+ /**
76
+ * Maps `harness-send`. A message sent while idle, or queued behind the running turn, becomes a
77
+ * queued prompt. A steer joins the running turn; with no running turn it is the next turn's
78
+ * input, so it is queued as well. A steer keeps the prompt options for that case only: it answers
79
+ * on the running turn's model. A steer id comes from `crypto.randomUUID()`, which browsers offer
80
+ * in secure contexts only (HTTPS or localhost).
81
+ */
82
+ export function flexHarnessSendIntent(
83
+ target: IFlexHarnessChatTarget,
84
+ detail: plugins.IHarnessComposerSendDetail,
85
+ options: IFlexHarnessSendIntentOptions = {},
86
+ ): TFlexHarnessChatIntent {
87
+ const prompt: TFlexPromptPart[] = [
88
+ ...(detail.text.length > 0 ? [{ type: 'text' as const, text: detail.text }] : []),
89
+ ...detail.attachments.map(attachmentToPromptPart),
90
+ ];
91
+ const modelHint = options.modelHint ?? detail.model;
92
+ const promptOptions: IFlexPromptOptions = modelHint === undefined ? {} : { modelHint };
93
+ if (detail.delivery === 'steer' && options.activeQueueId !== undefined) {
94
+ return {
95
+ ...target,
96
+ type: 'steerPrompt',
97
+ queueId: options.activeQueueId,
98
+ steerId: globalThis.crypto.randomUUID(),
99
+ prompt,
100
+ options: promptOptions,
101
+ };
102
+ }
103
+ return { ...target, type: 'enqueuePrompt', prompt, options: promptOptions };
104
+ }
105
+
106
+ /**
107
+ * Maps `harness-permission-response`. The card reports a remembered approval as `remember`,
108
+ * which FlexHarness calls `always`.
109
+ */
110
+ export function flexHarnessPermissionIntent(
111
+ target: IFlexHarnessChatTarget,
112
+ detail: plugins.IHarnessPermissionResponseDetail,
113
+ ): TFlexHarnessChatIntent {
114
+ const decision: TFlexPermissionDecision = detail.response === 'reject'
115
+ ? 'reject'
116
+ : detail.remember ? 'always' : 'once';
117
+ return { ...target, type: 'respondToPermission', permissionId: detail.requestId, decision };
118
+ }
119
+
120
+ /** Maps `harness-abort`. */
121
+ export function flexHarnessAbortIntent(target: IFlexHarnessChatTarget): TFlexHarnessChatIntent {
122
+ return { ...target, type: 'abort' };
123
+ }
124
+
125
+ /** The harness calls `runFlexHarnessChatIntent` makes. */
126
+ export type TFlexHarnessChatHost = Pick<
127
+ FlexHarness<unknown>,
128
+ 'enqueuePrompt' | 'steerPrompt' | 'respondToPermission' | 'abort'
129
+ >;
130
+
131
+ const steerNotRunningCode = 'FLEX_STEER_NOT_RUNNING';
132
+
133
+ /**
134
+ * Makes the call an intent names, on the server that owns the harness, once the harness admitted
135
+ * it. A queued prompt returns its admission: its `completion` settles when the run ends, and the
136
+ * caller owns it, as with `enqueuePrompt()`. A steer whose turn ended before the harness took it
137
+ * is the next turn's input, so it is queued instead and returns that admission.
138
+ */
139
+ export async function runFlexHarnessChatIntent(
140
+ harness: TFlexHarnessChatHost,
141
+ intent: TFlexHarnessChatIntent,
142
+ ): Promise<IFlexPromptQueueAdmission | undefined> {
143
+ switch (intent.type) {
144
+ case 'enqueuePrompt':
145
+ return harness.enqueuePrompt(intent.scopeId, intent.sessionId, intent.prompt, intent.options);
146
+ case 'steerPrompt':
147
+ try {
148
+ await harness.steerPrompt(intent.scopeId, intent.sessionId, intent.queueId, {
149
+ steerId: intent.steerId,
150
+ prompt: intent.prompt,
151
+ });
152
+ return undefined;
153
+ } catch (error) {
154
+ if ((error as { code?: unknown }).code !== steerNotRunningCode) throw error;
155
+ return harness.enqueuePrompt(intent.scopeId, intent.sessionId, intent.prompt, intent.options);
156
+ }
157
+ case 'respondToPermission':
158
+ await harness.respondToPermission(
159
+ intent.scopeId,
160
+ intent.sessionId,
161
+ intent.permissionId,
162
+ intent.decision,
163
+ );
164
+ return undefined;
165
+ case 'abort':
166
+ await harness.abort(intent.scopeId, intent.sessionId);
167
+ return undefined;
168
+ }
169
+ }