@modelprofile.com/flexharness-agent 9.9.1 → 10.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_ts_agent/classes.sessionengine.d.ts +3 -2
- package/dist_ts_agent/classes.sessionengine.js +208 -20
- package/dist_ts_agent/index.d.ts +4 -2
- package/dist_ts_agent/index.js +3 -2
- package/dist_ts_agent/smartagent.events.d.ts +9 -0
- package/dist_ts_agent/smartagent.events.js +1 -1
- package/dist_ts_agent/smartagent.interfaces.d.ts +30 -2
- package/dist_ts_agent/smartagent.interfaces.js +1 -1
- package/dist_ts_agent/smartagent.pendinginput.d.ts +58 -0
- package/dist_ts_agent/smartagent.pendinginput.js +92 -0
- package/dist_ts_agent/smartagent.persistence.js +9 -1
- package/dist_ts_agent/smartagent.transactions.d.ts +9 -0
- package/dist_ts_agent/smartagent.transactions.js +92 -5
- package/package.json +5 -5
- package/readme.md +27 -0
- package/ts_agent/classes.sessionengine.ts +210 -11
- package/ts_agent/index.ts +11 -0
- package/ts_agent/readme.md +27 -0
- package/ts_agent/smartagent.events.ts +9 -0
- package/ts_agent/smartagent.interfaces.ts +31 -1
- package/ts_agent/smartagent.pendinginput.ts +116 -0
- package/ts_agent/smartagent.persistence.ts +9 -0
- package/ts_agent/smartagent.transactions.ts +106 -5
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
TAgentToolResultOutput,
|
|
18
18
|
} from './smartagent.events.js';
|
|
19
19
|
import type { TAgentEventArchive, TAgentEventStore } from './smartagent.persistence.js';
|
|
20
|
+
import type { AgentPendingInputQueue, IAgentAppliedInput } from './smartagent.pendinginput.js';
|
|
20
21
|
import type { IAgentRetryEvent } from './smartagent.retry.js';
|
|
21
22
|
|
|
22
23
|
export type { ProviderOptions };
|
|
@@ -129,7 +130,7 @@ export type TAgentContextBuilder = (
|
|
|
129
130
|
|
|
130
131
|
export interface IAgentContextCompactionOptions {
|
|
131
132
|
abortSignal?: AbortSignal;
|
|
132
|
-
reason: 'context-overflow' | 'retention' | 'manual';
|
|
133
|
+
reason: 'context-overflow' | 'context-budget' | 'retention' | 'manual';
|
|
133
134
|
/**
|
|
134
135
|
* Reports the usage of each model call the compactor makes, into the session's `onUsage` as
|
|
135
136
|
* `source: 'compaction'`. The session always supplies it. Report every call before the returned
|
|
@@ -189,6 +190,11 @@ export interface IAgentSessionOptions {
|
|
|
189
190
|
contextBuilder?: TAgentContextBuilder;
|
|
190
191
|
/** Explicit context compactor used by manual and event-retention compaction. */
|
|
191
192
|
contextCompactor?: TAgentContextCompactor;
|
|
193
|
+
/** Compact settled history before inference when model messages exceed this UTF-8 JSON byte budget.
|
|
194
|
+
* Defaults to 256 KiB when a compactor is supplied; false disables proactive compaction.
|
|
195
|
+
* The current unsettled generation is never summarized or archived.
|
|
196
|
+
*/
|
|
197
|
+
contextCompactionBytes?: number | false;
|
|
192
198
|
/** Optional bounded active-event policy. Requires contextCompactor and eventStore.archive. */
|
|
193
199
|
eventRetention?: IAgentEventRetentionOptions;
|
|
194
200
|
/** Maximum wait for each session change listener. Default: 30 seconds. */
|
|
@@ -273,6 +279,14 @@ export interface IAgentBeginGenerationOptions {
|
|
|
273
279
|
generationId?: string;
|
|
274
280
|
}
|
|
275
281
|
|
|
282
|
+
export interface IAgentFinalizeGenerationOptions {
|
|
283
|
+
/**
|
|
284
|
+
* With outcome `interrupted`: the owner stopped the generation, so what its model produced
|
|
285
|
+
* stays in the model context.
|
|
286
|
+
*/
|
|
287
|
+
stopped?: boolean;
|
|
288
|
+
}
|
|
289
|
+
|
|
276
290
|
export interface IAgentGenerationPreparationContext {
|
|
277
291
|
generationId: string;
|
|
278
292
|
abortSignal: AbortSignal;
|
|
@@ -337,6 +351,13 @@ export interface IAgentGenerateOptions {
|
|
|
337
351
|
maxSteps?: number;
|
|
338
352
|
/** Cancels only this generation and its synchronous tool executions. */
|
|
339
353
|
abort?: AbortSignal;
|
|
354
|
+
/**
|
|
355
|
+
* Stops a transactional generation the way a user stops a turn. It ends the generation like
|
|
356
|
+
* `abort`, but the interrupted outcome records the stop: the text the running model step
|
|
357
|
+
* streamed and the tool calls it issued are kept, and the generation stays in the model
|
|
358
|
+
* context once its model produced output. An aborted or failed generation is not kept.
|
|
359
|
+
*/
|
|
360
|
+
stop?: AbortSignal;
|
|
340
361
|
/** Opt-in transactional claim returned by beginGeneration(). */
|
|
341
362
|
transaction?: IAgentGenerationHandle;
|
|
342
363
|
/**
|
|
@@ -346,6 +367,14 @@ export interface IAgentGenerateOptions {
|
|
|
346
367
|
* The callback owns cleanup of partial acquisition if it throws before returning a lease.
|
|
347
368
|
*/
|
|
348
369
|
prepare?: TAgentGenerationPrepare;
|
|
370
|
+
/**
|
|
371
|
+
* User input the generation takes in at its step boundaries, recorded as user messages of
|
|
372
|
+
* the generation. The generation closes the queue when it ends; `close()` then returns the
|
|
373
|
+
* inputs it never took in.
|
|
374
|
+
*/
|
|
375
|
+
pendingInput?: AgentPendingInputQueue;
|
|
376
|
+
/** Called once per input taken from `pendingInput`, after it is durable and before the next model call. */
|
|
377
|
+
onInputApplied?: (input: IAgentAppliedInput) => void | Promise<void>;
|
|
349
378
|
}
|
|
350
379
|
|
|
351
380
|
export interface IAgentScheduleGenerateOptions extends IAgentGenerateOptions {
|
|
@@ -413,6 +442,7 @@ export interface IAgentSession {
|
|
|
413
442
|
finalizeGeneration(
|
|
414
443
|
transaction: IAgentGenerationHandle,
|
|
415
444
|
outcome: TAgentGenerationOutcome,
|
|
445
|
+
options?: IAgentFinalizeGenerationOptions,
|
|
416
446
|
): Promise<void>;
|
|
417
447
|
listUncertainToolExecutions(): IToolExecutionIntentEvent[];
|
|
418
448
|
reconcileToolExecution(
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { TAgentPrompt } from './smartagent.interfaces.js';
|
|
2
|
+
|
|
3
|
+
/** One user input that a running generation takes in at its next step boundary. */
|
|
4
|
+
export interface IAgentPendingInput {
|
|
5
|
+
/** Caller-chosen identity, unique within its queue. */
|
|
6
|
+
inputId: string;
|
|
7
|
+
content: TAgentPrompt;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** A pending input the generation recorded as a user message of its own. */
|
|
11
|
+
export interface IAgentAppliedInput {
|
|
12
|
+
inputId: string;
|
|
13
|
+
/** The `user-message` event that carries the input. */
|
|
14
|
+
eventId: string;
|
|
15
|
+
generationId: string;
|
|
16
|
+
/** Model steps the generation had completed when it took the input in. */
|
|
17
|
+
completedSteps: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type TAgentPendingInputRejection = 'duplicate' | 'closed';
|
|
21
|
+
|
|
22
|
+
/** The queue refused an input: its id was used before, or its generation has ended. */
|
|
23
|
+
export class AgentPendingInputRejectedError extends Error {
|
|
24
|
+
constructor(
|
|
25
|
+
public readonly reason: TAgentPendingInputRejection,
|
|
26
|
+
public readonly inputId: string,
|
|
27
|
+
) {
|
|
28
|
+
super(reason === 'duplicate'
|
|
29
|
+
? `Pending input "${inputId}" was already added.`
|
|
30
|
+
: `Pending input "${inputId}" arrived after its generation stopped taking input.`);
|
|
31
|
+
this.name = 'AgentPendingInputRejectedError';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const assertPrompt = (content: TAgentPrompt): void => {
|
|
36
|
+
if (typeof content === 'string') {
|
|
37
|
+
if (!content.trim()) throw new Error('Pending input content must not be empty.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
41
|
+
throw new Error('Pending input content must be a non-empty string or part list.');
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The user input a generation takes in while it runs. A generation that receives the queue
|
|
47
|
+
* through `generate({ pendingInput })` drains it at every step boundary, after the tool results
|
|
48
|
+
* of its previous model step are recorded and before its next model call, and records each
|
|
49
|
+
* input as a user message of its own. A running tool call or permission wait is never
|
|
50
|
+
* interrupted: its input waits for the boundary that follows it.
|
|
51
|
+
*
|
|
52
|
+
* `close()` ends intake and returns the inputs the generation never took in: the ones still
|
|
53
|
+
* waiting, and the ones it drained but could not record durably, which it `restore()`s. An input
|
|
54
|
+
* added after it is refused with reason `closed`. The generation closes the queue when it ends,
|
|
55
|
+
* so every input is either applied or returned by `close()`. Once the generation has settled,
|
|
56
|
+
* `close()` returns the same inputs on every call; before that, a batch the generation `restore()`s
|
|
57
|
+
* after a failed save is added to what it returns.
|
|
58
|
+
*/
|
|
59
|
+
export class AgentPendingInputQueue {
|
|
60
|
+
/** Inputs not taken in, in arrival order. */
|
|
61
|
+
private readonly waiting: IAgentPendingInput[] = [];
|
|
62
|
+
private readonly inputIds = new Set<string>();
|
|
63
|
+
private isClosed = false;
|
|
64
|
+
|
|
65
|
+
/** True once `close()` ended intake. */
|
|
66
|
+
public get closed(): boolean {
|
|
67
|
+
return this.isClosed;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Inputs waiting for the next step boundary. */
|
|
71
|
+
public get size(): number {
|
|
72
|
+
return this.isClosed ? 0 : this.waiting.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public add(input: IAgentPendingInput): void {
|
|
76
|
+
if (typeof input.inputId !== 'string' || !input.inputId.trim()) {
|
|
77
|
+
throw new Error('Pending input id must be a non-empty string.');
|
|
78
|
+
}
|
|
79
|
+
assertPrompt(input.content);
|
|
80
|
+
if (this.inputIds.has(input.inputId)) {
|
|
81
|
+
throw new AgentPendingInputRejectedError('duplicate', input.inputId);
|
|
82
|
+
}
|
|
83
|
+
if (this.isClosed) throw new AgentPendingInputRejectedError('closed', input.inputId);
|
|
84
|
+
this.inputIds.add(input.inputId);
|
|
85
|
+
this.waiting.push({ inputId: input.inputId, content: structuredClone(input.content) });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Takes every waiting input in arrival order; a closed queue has none left to take. */
|
|
89
|
+
public drain(): IAgentPendingInput[] {
|
|
90
|
+
if (this.isClosed) return [];
|
|
91
|
+
return this.waiting.splice(0, this.waiting.length);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gives back inputs a generation drained but could not record durably. They count as never
|
|
96
|
+
* taken in and go before the inputs that arrived after them; a closed queue returns them from
|
|
97
|
+
* `close()`, an open one hands them out again at the next `drain()`.
|
|
98
|
+
*/
|
|
99
|
+
public restore(inputs: readonly IAgentPendingInput[]): void {
|
|
100
|
+
for (const input of inputs) {
|
|
101
|
+
if (!this.inputIds.has(input.inputId)) {
|
|
102
|
+
throw new Error(`Pending input "${input.inputId}" was never added to this queue.`);
|
|
103
|
+
}
|
|
104
|
+
if (this.waiting.some((waiting) => waiting.inputId === input.inputId)) {
|
|
105
|
+
throw new Error(`Pending input "${input.inputId}" is still waiting.`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.waiting.unshift(...inputs);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Ends intake and returns the inputs that were never taken in, in arrival order. */
|
|
112
|
+
public close(): readonly IAgentPendingInput[] {
|
|
113
|
+
this.isClosed = true;
|
|
114
|
+
return Object.freeze([...this.waiting]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
@@ -392,6 +392,9 @@ const validateAgentEvents = (
|
|
|
392
392
|
? 'user'
|
|
393
393
|
: 'assistant');
|
|
394
394
|
assertProviderOptions(eventValue.providerOptions, `${eventPath}.providerOptions`);
|
|
395
|
+
if (eventValue.type === 'user-message' && eventValue.inputId !== undefined) {
|
|
396
|
+
assertNonEmptyString(eventValue.inputId, `${eventPath}.inputId`);
|
|
397
|
+
}
|
|
395
398
|
break;
|
|
396
399
|
case 'tool-call':
|
|
397
400
|
assertNonEmptyString(eventValue.toolCallId, `${eventPath}.toolCallId`);
|
|
@@ -447,6 +450,9 @@ const validateAgentEvents = (
|
|
|
447
450
|
&& eventValue.outcome !== 'interrupted'
|
|
448
451
|
) validationError(`${eventPath}.outcome`, 'is invalid.');
|
|
449
452
|
assertOptionalString(eventValue.reason, `${eventPath}.reason`);
|
|
453
|
+
if (eventValue.stopped !== undefined && (
|
|
454
|
+
eventValue.stopped !== true || eventValue.outcome !== 'interrupted'
|
|
455
|
+
)) validationError(`${eventPath}.stopped`, 'is only valid as true on an interrupted outcome.');
|
|
450
456
|
break;
|
|
451
457
|
case 'tool-execution-intent':
|
|
452
458
|
assertRequiredGenerationId(eventValue, eventPath);
|
|
@@ -470,6 +476,9 @@ const validateAgentEvents = (
|
|
|
470
476
|
}
|
|
471
477
|
break;
|
|
472
478
|
case 'context-compaction':
|
|
479
|
+
if (eventValue.automatic !== undefined && eventValue.automatic !== true) {
|
|
480
|
+
validationError(`${eventPath}.automatic`, 'must be true when present.');
|
|
481
|
+
}
|
|
473
482
|
assertStringArray(eventValue.coveredEventIds, `${eventPath}.coveredEventIds`);
|
|
474
483
|
validateAgentEvents(eventValue.replacementEvents, `${eventPath}.replacementEvents`, true);
|
|
475
484
|
if (eventValue.archivedTransactions !== undefined) {
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
IGenerationExecutionStartedEvent,
|
|
5
5
|
IGenerationOutcomeEvent,
|
|
6
6
|
IToolExecutionIntentEvent,
|
|
7
|
+
IToolResultEvent,
|
|
7
8
|
TAgentControlEvent,
|
|
8
9
|
TAgentEvent,
|
|
9
10
|
TAgentGenerationOutcome,
|
|
@@ -138,11 +139,104 @@ export const isTransactionalGeneration = (
|
|
|
138
139
|
generationId: string,
|
|
139
140
|
): boolean => getAgentGenerationTransactions(events).has(generationId);
|
|
140
141
|
|
|
142
|
+
/** The model output that closes a tool call its stopped generation left without a result. */
|
|
143
|
+
export const INTERRUPTED_TOOL_CALL_RESULT =
|
|
144
|
+
'The tool call was interrupted before its result was recorded; its effect is unknown.';
|
|
145
|
+
|
|
146
|
+
const isModelOutputEvent = (event: TAgentEvent): boolean =>
|
|
147
|
+
event.type === 'assistant-message'
|
|
148
|
+
|| event.type === 'tool-call'
|
|
149
|
+
|| event.type === 'tool-result'
|
|
150
|
+
|| event.type === 'model-message';
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether the events of a transactional generation are part of the model context. An accepted
|
|
154
|
+
* generation is, and so is the generation that is executing. A generation its owner stopped
|
|
155
|
+
* stays in the context once the model produced output in it: its prompt, the input it took in
|
|
156
|
+
* while it ran and what the model produced before the stop. One stopped before any model output
|
|
157
|
+
* left no trace in the conversation and is not shown, so its prompt can be sent again. Rejected
|
|
158
|
+
* generations, and generations interrupted by an abort, a failure or a restart, are not shown.
|
|
159
|
+
*/
|
|
160
|
+
const isModelVisibleTransaction = (
|
|
161
|
+
transaction: IAgentGenerationTransaction,
|
|
162
|
+
producedOutput: ReadonlySet<string>,
|
|
163
|
+
): boolean =>
|
|
164
|
+
transaction.state === 'accepted'
|
|
165
|
+
|| transaction.state === 'execution-started'
|
|
166
|
+
|| (
|
|
167
|
+
transaction.state === 'interrupted'
|
|
168
|
+
&& transaction.outcome?.stopped === true
|
|
169
|
+
&& transaction.executionStarted !== undefined
|
|
170
|
+
&& producedOutput.has(transaction.generationId)
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
/** The generations whose model produced output in `events`. */
|
|
174
|
+
const generationsWithModelOutput = (events: readonly TAgentEvent[]): Set<string> => {
|
|
175
|
+
const producedOutput = new Set<string>();
|
|
176
|
+
for (const event of events) {
|
|
177
|
+
if (event.generationId && isModelOutputEvent(event)) producedOutput.add(event.generationId);
|
|
178
|
+
}
|
|
179
|
+
return producedOutput;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Whether the transactional generation `generationId` is part of the model context of later
|
|
184
|
+
* generations, by the rule `filterModelVisibleAgentEvents` applies: it was accepted, is
|
|
185
|
+
* executing, or was stopped by its owner after its model produced output. False for a
|
|
186
|
+
* generation `events` holds no transaction of.
|
|
187
|
+
*/
|
|
188
|
+
export const isGenerationInModelContext = (
|
|
189
|
+
events: readonly TAgentEvent[],
|
|
190
|
+
generationId: string,
|
|
191
|
+
): boolean => {
|
|
192
|
+
const transaction = getAgentGenerationTransactions(events).get(generationId);
|
|
193
|
+
return transaction !== undefined
|
|
194
|
+
&& isModelVisibleTransaction(transaction, generationsWithModelOutput(events));
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const interruptedToolResult = (
|
|
198
|
+
toolCallId: string,
|
|
199
|
+
toolName: string,
|
|
200
|
+
source: TAgentEvent,
|
|
201
|
+
): IToolResultEvent => ({
|
|
202
|
+
id: `${source.id}:interrupted:${toolCallId}`,
|
|
203
|
+
timestamp: source.timestamp,
|
|
204
|
+
type: 'tool-result',
|
|
205
|
+
toolCallId,
|
|
206
|
+
toolName,
|
|
207
|
+
error: INTERRUPTED_TOOL_CALL_RESULT,
|
|
208
|
+
modelOutput: { type: 'error-text', value: INTERRUPTED_TOOL_CALL_RESULT },
|
|
209
|
+
generationId: source.generationId,
|
|
210
|
+
inferenceId: source.inferenceId,
|
|
211
|
+
causationId: source.causationId,
|
|
212
|
+
parentIds: [source.id],
|
|
213
|
+
});
|
|
214
|
+
|
|
141
215
|
export const filterModelVisibleAgentEvents = (
|
|
142
216
|
events: readonly TAgentEvent[],
|
|
143
217
|
): TAgentEvent[] => {
|
|
144
218
|
const transactions = getAgentGenerationTransactions(events);
|
|
219
|
+
const producedOutput = generationsWithModelOutput(events);
|
|
220
|
+
const resultedToolCalls = new Set<string>();
|
|
221
|
+
for (const event of events) {
|
|
222
|
+
if (!event.generationId) continue;
|
|
223
|
+
if (event.type === 'tool-result') resultedToolCalls.add(`${event.generationId}\0${event.toolCallId}`);
|
|
224
|
+
if (event.type === 'assistant-message' && Array.isArray(event.content)) {
|
|
225
|
+
for (const part of event.content) {
|
|
226
|
+
if (part.type === 'tool-result') {
|
|
227
|
+
resultedToolCalls.add(`${event.generationId}\0${part.toolCallId}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
145
232
|
const visibleEvents: TAgentEvent[] = [];
|
|
233
|
+
/** Closes each tool call a stopped generation left open, right after the call. */
|
|
234
|
+
const closeOpenToolCall = (toolCallId: string, toolName: string, source: TAgentEvent): void => {
|
|
235
|
+
const key = `${source.generationId}\0${toolCallId}`;
|
|
236
|
+
if (resultedToolCalls.has(key)) return;
|
|
237
|
+
resultedToolCalls.add(key);
|
|
238
|
+
visibleEvents.push(interruptedToolResult(toolCallId, toolName, source));
|
|
239
|
+
};
|
|
146
240
|
for (const event of events) {
|
|
147
241
|
if (isControlEvent(event)) continue;
|
|
148
242
|
if (event.type === 'context-compaction' && event.archivedTransactions) {
|
|
@@ -154,11 +248,18 @@ export const filterModelVisibleAgentEvents = (
|
|
|
154
248
|
continue;
|
|
155
249
|
}
|
|
156
250
|
const transaction = transactions.get(event.generationId);
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
251
|
+
if (transaction && !isModelVisibleTransaction(transaction, producedOutput)) continue;
|
|
252
|
+
visibleEvents.push(event);
|
|
253
|
+
if (transaction?.state !== 'interrupted') continue;
|
|
254
|
+
if (event.type === 'tool-call' && event.providerExecuted !== true) {
|
|
255
|
+
closeOpenToolCall(event.toolCallId, event.toolName, event);
|
|
256
|
+
} else if (event.type === 'assistant-message' && Array.isArray(event.content)) {
|
|
257
|
+
for (const part of event.content) {
|
|
258
|
+
if (part.type === 'tool-call' && part.providerExecuted !== true) {
|
|
259
|
+
closeOpenToolCall(part.toolCallId, part.toolName, event);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
162
263
|
}
|
|
163
264
|
return visibleEvents;
|
|
164
265
|
};
|