@inflexa-ai/harness 0.48.2 → 0.48.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app/chat-turn-workflow.d.ts +3 -1
- package/dist/app/chat-turn-workflow.js +18 -3
- package/dist/app/chat-turn.js +27 -1
- package/dist/app/in-process-turn.js +3 -6
- package/dist/contracts/chat-events.d.ts +5 -0
- package/dist/contracts/chat-parts.d.ts +19 -1
- package/dist/contracts/chat-parts.js +1 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/schemas/chat-events.d.ts +2 -0
- package/dist/contracts/schemas/chat-events.js +1 -0
- package/dist/contracts/schemas/chat-parts.d.ts +29 -1
- package/dist/contracts/schemas/chat-parts.js +10 -0
- package/dist/index.d.ts +1 -1
- package/dist/loop/chat-stream.js +1 -1
- package/dist/loop/run-loop.js +16 -0
- package/dist/memory/conversation-display-storage.d.ts +3 -2
- package/dist/memory/conversation-display-storage.js +10 -1
- package/dist/providers/ai-sdk.d.ts +17 -14
- package/dist/providers/ai-sdk.js +14 -16
- package/package.json +1 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { AuthContext } from "../auth/types.js";
|
|
17
17
|
import type { TokenUsageRollup } from "../contracts/usage.js";
|
|
18
|
-
import type { ChatFrame } from "../contracts/chat-events.js";
|
|
18
|
+
import type { ChatErrorEvent, ChatFrame, EventSource, FinishEvent } from "../contracts/chat-events.js";
|
|
19
19
|
import type { AgentDefinition } from "../loop/types.js";
|
|
20
20
|
import type { ThreadType } from "../memory/thread-store.js";
|
|
21
21
|
import type { PromptCachePolicy } from "../providers/types.js";
|
|
@@ -110,5 +110,7 @@ export declare const DEFAULT_ASK_WAIT_SECONDS: number;
|
|
|
110
110
|
* finds the workflow of a recovery by its registered name.
|
|
111
111
|
*/
|
|
112
112
|
export declare function registerChatTurns(deps: ChatTurnWorkflowDeps): ChatTurns;
|
|
113
|
+
/** The last frame of a turn that ran: `error` for a failed turn, else `finish` with the stop reason of the turn. */
|
|
114
|
+
export declare function endFrameOf(ran: ChatTurnRan, rootSource: EventSource): ChatErrorEvent | FinishEvent;
|
|
113
115
|
/** How a turn that ran ended, as a host reads it. */
|
|
114
116
|
export declare function chatTurnEndOf(ran: ChatTurnRan): ChatTurnEnd;
|
|
@@ -277,9 +277,7 @@ async function runTurnWorkflow(deps, input, signal) {
|
|
|
277
277
|
};
|
|
278
278
|
const ran = await runOpenTurn(deps, { analysisId: input.analysisId, threadId: input.threadId, userInput: input.userText }, { kind: "ok", threadType: input.threadType, history: view, userMessage: opening.userMessage, contextRecords: opening.contextRecords }, agent.value, { session, author: input.author, startedAtMs: input.startedAtMs, promptCache: input.promptCache }, transport);
|
|
279
279
|
const { outcome } = ran;
|
|
280
|
-
await writer.write(
|
|
281
|
-
? { type: "error", message: outcome.reason, reason: outcome.code, source: rootSource }
|
|
282
|
-
: { type: "finish", source: rootSource, turnUsage: ran.turnUsage });
|
|
280
|
+
await writer.write(endFrameOf(ran, rootSource));
|
|
283
281
|
await writer.settle();
|
|
284
282
|
await closeDurableStream(CHAT_STREAM_KEY, outcome.status === "done" ? "stop" : outcome.status === "aborted" ? "aborted" : "error");
|
|
285
283
|
// A sub-agent that a stop canceled in its pause leaves its row, thus the end of the turn clears each row of the turn.
|
|
@@ -287,6 +285,23 @@ async function runTurnWorkflow(deps, input, signal) {
|
|
|
287
285
|
await DBOS.runStep(async () => unwrapOrThrow(await releaseThreadOfTurn(deps.pool, input.threadId, input.turnId)), { name: "chat-turn.release" });
|
|
288
286
|
return chatTurnEndOf(ran);
|
|
289
287
|
}
|
|
288
|
+
/** The last frame of a turn that ran: `error` for a failed turn, else `finish` with the stop reason of the turn. */
|
|
289
|
+
export function endFrameOf(ran, rootSource) {
|
|
290
|
+
const { outcome } = ran;
|
|
291
|
+
switch (outcome.status) {
|
|
292
|
+
case "failed":
|
|
293
|
+
return { type: "error", message: outcome.reason, reason: outcome.code, source: rootSource };
|
|
294
|
+
case "done":
|
|
295
|
+
return { type: "finish", source: rootSource, finishReason: outcome.finish.reason, turnUsage: ran.turnUsage };
|
|
296
|
+
// An abort that cut the run before its end leaves no finish of the loop.
|
|
297
|
+
case "aborted":
|
|
298
|
+
return { type: "finish", source: rootSource, finishReason: "aborted", turnUsage: ran.turnUsage };
|
|
299
|
+
default: {
|
|
300
|
+
const exhaustive = outcome;
|
|
301
|
+
throw new Error(`unhandled turn outcome: ${JSON.stringify(exhaustive)}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
290
305
|
/** How a turn that ran ended, as a host reads it. */
|
|
291
306
|
export function chatTurnEndOf(ran) {
|
|
292
307
|
const { outcome } = ran;
|
package/dist/app/chat-turn.js
CHANGED
|
@@ -218,7 +218,7 @@ export async function runOpenTurn(deps, turn, prepared, agent, params, transport
|
|
|
218
218
|
await flush({
|
|
219
219
|
status: outcome.status,
|
|
220
220
|
reason: outcome.status === "failed" ? outcome.reason : undefined,
|
|
221
|
-
note:
|
|
221
|
+
note: closingNoteOf(outcome, recorder),
|
|
222
222
|
turnUsage,
|
|
223
223
|
turnDurationMs: durationMs,
|
|
224
224
|
});
|
|
@@ -292,6 +292,32 @@ function failureReasonOf(err) {
|
|
|
292
292
|
return findProviderError(err) === undefined ? "The turn stopped on an internal error." : "The model request failed.";
|
|
293
293
|
}
|
|
294
294
|
}
|
|
295
|
+
/** The note that closes a turn: a failed, an aborted, or a declined turn has one, and a turn that the model answered has none. */
|
|
296
|
+
function closingNoteOf(outcome, recorder) {
|
|
297
|
+
switch (outcome.status) {
|
|
298
|
+
case "failed":
|
|
299
|
+
return conversationRecordTurn(failureNote(outcome.reason));
|
|
300
|
+
case "aborted":
|
|
301
|
+
return interruptionNote();
|
|
302
|
+
case "done":
|
|
303
|
+
return outcome.finish.reason === "content-filter" ? declineNote(recorder) : undefined;
|
|
304
|
+
default: {
|
|
305
|
+
const exhaustive = outcome;
|
|
306
|
+
throw new Error(`unhandled turn outcome: ${JSON.stringify(exhaustive)}`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* The note of a turn that the content filter stopped. A declined request often gives no message, and a round
|
|
312
|
+
* with no message gives no row. The note is the row that holds the display of such a round, thus a reload
|
|
313
|
+
* shows the refusal. It also tells the next turn that this request has no reply.
|
|
314
|
+
*/
|
|
315
|
+
function declineNote(recorder) {
|
|
316
|
+
return {
|
|
317
|
+
modelMessages: [syntheticUserMessage("[The model provider declined the request above, thus it has no reply.]")],
|
|
318
|
+
displayMessages: recorder.takeRound([]),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
295
321
|
/**
|
|
296
322
|
* The note of an aborted turn. Without it, the next turn reads the request of the aborted turn as open, and
|
|
297
323
|
* its reply also answers that request. The transcript shows the interruption from the turn record, thus the
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { toChatFrame } from "../contracts/chat-frame.js";
|
|
9
|
-
import { chatTurnEndOf } from "./chat-turn-workflow.js";
|
|
9
|
+
import { chatTurnEndOf, endFrameOf } from "./chat-turn-workflow.js";
|
|
10
10
|
import { createConversationDisplayRecorder } from "../memory/conversation-display-recorder.js";
|
|
11
11
|
import { prepareChatTurn, runOpenTurn, userTextOf, } from "./chat-turn.js";
|
|
12
12
|
/** Prepare a turn and resolve its agent. A refusal comes back before any row of the turn exists. */
|
|
@@ -112,11 +112,8 @@ export function inProcessChatTurns(deps) {
|
|
|
112
112
|
promptCache: request.promptCache,
|
|
113
113
|
})
|
|
114
114
|
.then((ran) => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
? { type: "error", message: turnEnd.reason ?? "The turn failed.", reason: turnEnd.code, source: rootSource }
|
|
118
|
-
: { type: "finish", source: rootSource, turnUsage: turnEnd.turnUsage });
|
|
119
|
-
return turnEnd;
|
|
115
|
+
log.push(endFrameOf(ran, rootSource));
|
|
116
|
+
return chatTurnEndOf(ran);
|
|
120
117
|
})
|
|
121
118
|
.catch(() => null)
|
|
122
119
|
.finally(() => log.close());
|
|
@@ -162,6 +162,11 @@ export type ToolFinishedEvent = {
|
|
|
162
162
|
export type FinishEvent = {
|
|
163
163
|
type: "finish";
|
|
164
164
|
source: EventSource;
|
|
165
|
+
/**
|
|
166
|
+
* Why the turn stopped: the finish reason of the last reply, for example `stop` or `content-filter`, or `aborted`,
|
|
167
|
+
* `max_iterations`, or `denied`. A word outside these stays as the provider sent it.
|
|
168
|
+
*/
|
|
169
|
+
finishReason: string;
|
|
165
170
|
/** What this loop's own LLM calls used. */
|
|
166
171
|
usage?: TokenUsageRollup;
|
|
167
172
|
/**
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* data-plan-approval (the record of an approved plan)
|
|
10
10
|
* Compaction: data-compaction (a compaction of the conversation thread)
|
|
11
11
|
* Model retry: data-model-retry (a retry of a model request inside the retry envelope of the provider)
|
|
12
|
+
* Model refusal: data-model-refusal (a model request that the provider declined)
|
|
12
13
|
* Run lifecycle: run-started, dag-state, run-completed, run-failed, run-synthesis,
|
|
13
14
|
* synthesis-progress
|
|
14
15
|
* Per-step live: step-activity, step-file-tree
|
|
@@ -498,6 +499,23 @@ export type ModelRetryPart = {
|
|
|
498
499
|
/** The HTTP status of the failed attempt. Absent for a failure with no status, for example a lost connection. */
|
|
499
500
|
status?: number;
|
|
500
501
|
};
|
|
502
|
+
/**
|
|
503
|
+
* A model request that the provider declined. `fallback`: another model answered the request, and `model` names it.
|
|
504
|
+
* `stopped`: no model answered, thus the reply of the request ended with the content filter.
|
|
505
|
+
*/
|
|
506
|
+
export type ModelRefusal = {
|
|
507
|
+
/** The id of the model response. */
|
|
508
|
+
id: string;
|
|
509
|
+
outcome: "fallback";
|
|
510
|
+
model: string;
|
|
511
|
+
} | {
|
|
512
|
+
/** The id of the model response. */
|
|
513
|
+
id: string;
|
|
514
|
+
outcome: "stopped";
|
|
515
|
+
};
|
|
516
|
+
export type ModelRefusalPart = {
|
|
517
|
+
type: "data-model-refusal";
|
|
518
|
+
} & ModelRefusal;
|
|
501
519
|
/** The kind of a durable work, and of the work that a pause belongs to. A sub-agent belongs to its turn, and a sandbox step to its run. */
|
|
502
520
|
export type WorkKind = "turn" | "run" | "profile";
|
|
503
521
|
/**
|
|
@@ -521,7 +539,7 @@ export type PausePart = {
|
|
|
521
539
|
retryAt?: string;
|
|
522
540
|
};
|
|
523
541
|
/** The parts whose contracts this version of the package declares — the arms the part registry knows. */
|
|
524
|
-
export type KnownChatPart = PresentationPart | PlanPart | RunCardPart | PlanApprovalPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | ActivityEntryPart | SubAgentActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart | CompactionPart | PausePart | ModelRetryPart;
|
|
542
|
+
export type KnownChatPart = PresentationPart | PlanPart | RunCardPart | PlanApprovalPart | FileReferencePart | AskPart | RunStartedPart | DagStatePart | StepActivityPart | ActivityEntryPart | SubAgentActivityPart | StepFileTreePart | StepOutputPart | StepSummaryPart | StepUsagePart | StepBlockedPart | RunSynthesisPart | SynthesisProgressPart | RunCompletedPart | RunFailedPart | ChildSessionStartedPart | ReportRenderedPart | CompactionPart | PausePart | ModelRetryPart | ModelRefusalPart;
|
|
525
543
|
/**
|
|
526
544
|
* A part of a known contract, or a part whose type this build does not know. An emitter newer than this
|
|
527
545
|
* build can lead the registry, and such a part already rides through the check boundary and the live view
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* data-plan-approval (the record of an approved plan)
|
|
10
10
|
* Compaction: data-compaction (a compaction of the conversation thread)
|
|
11
11
|
* Model retry: data-model-retry (a retry of a model request inside the retry envelope of the provider)
|
|
12
|
+
* Model refusal: data-model-refusal (a model request that the provider declined)
|
|
12
13
|
* Run lifecycle: run-started, dag-state, run-completed, run-failed, run-synthesis,
|
|
13
14
|
* synthesis-progress
|
|
14
15
|
* Per-step live: step-activity, step-file-tree
|
|
@@ -2,7 +2,7 @@ export { ORGAN_SYSTEMS, OrganSystemSchema, ORGAN_SYSTEM_LABELS, organSystemLabel
|
|
|
2
2
|
export type { OrganSystem } from "./organ-system.js";
|
|
3
3
|
export { SEVERITIES, SeveritySchema } from "./severity.js";
|
|
4
4
|
export type { Severity } from "./severity.js";
|
|
5
|
-
export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCardPart, PlanApprovalPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, PlanGraphStep, DagStatePart, StepPhase, StepActivityPart, ActivityEntryPart, SubAgentActivityEntry, SubAgentActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, FindingReference, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, ChildSessionStartedPart, ReportRenderedPart, CompactionPart, PauseCause, WorkKind, PausePart, ModelRetryPart, ChatPart, KnownChatPart, } from "./chat-parts.js";
|
|
5
|
+
export type { PlanStep, PresentationContent, PresentationPart, PlanPart, RunCardPart, PlanApprovalPart, FileReferenceEntry, FileReferencePart, AskPart, RunStartedPart, StepStatus, DagStepState, PlanGraphStep, DagStatePart, StepPhase, StepActivityPart, ActivityEntryPart, SubAgentActivityEntry, SubAgentActivityPart, FileTreeEntry, StepFileTreePart, StepOutputFile, StepOutputPart, StepSummaryPart, StepUsagePart, StepBlockedPart, FindingReference, SynthesizedFinding, BiologicalTheme, RunSynthesisPart, SynthesisPhase, SynthesisProgressPart, RunCompletedFinding, RunCompletedPart, RunFailedPart, ChildSessionStartedPart, ReportRenderedPart, CompactionPart, PauseCause, WorkKind, PausePart, ModelRetryPart, ModelRefusal, ModelRefusalPart, ChatPart, KnownChatPart, } from "./chat-parts.js";
|
|
6
6
|
export type { EventSource, TextDeltaEvent, IterationEvent, ToolStartedEvent, ToolFinishedEvent, ToolCallLeaf, ToolCallValue, FinishEvent, ChatErrorEvent, MessageStartEvent, ChatEvent, ChatFrame, ChatPartFrame, } from "./chat-events.js";
|
|
7
7
|
export type { TokenUsageRollup } from "./usage.js";
|
|
8
8
|
export { DATA_PROFILE_RUN_LITERAL } from "./data-profile.js";
|
|
@@ -24,6 +24,7 @@ export const PART_REGISTRY = {
|
|
|
24
24
|
"data-compaction": { emitter: "conversation", consumer: "conversation", transient: true, reconciling: true },
|
|
25
25
|
"data-pause": { emitter: "workflow", consumer: "conversation", transient: false, reconciling: true },
|
|
26
26
|
"data-model-retry": { emitter: "conversation", consumer: "conversation", transient: true, reconciling: true },
|
|
27
|
+
"data-model-refusal": { emitter: "conversation", consumer: "conversation", transient: false, reconciling: false },
|
|
27
28
|
};
|
|
28
29
|
export function isTransient(type) {
|
|
29
30
|
return PART_REGISTRY[type].transient;
|
|
@@ -259,6 +259,7 @@ export declare const FinishEventSchema: z.ZodObject<{
|
|
|
259
259
|
callPath: z.ZodArray<z.ZodString>;
|
|
260
260
|
invocationId: z.ZodOptional<z.ZodString>;
|
|
261
261
|
}, z.core.$strip>;
|
|
262
|
+
finishReason: z.ZodString;
|
|
262
263
|
usage: z.ZodOptional<z.ZodObject<{
|
|
263
264
|
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
264
265
|
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
@@ -484,6 +485,7 @@ export declare const ChatEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
484
485
|
callPath: z.ZodArray<z.ZodString>;
|
|
485
486
|
invocationId: z.ZodOptional<z.ZodString>;
|
|
486
487
|
}, z.core.$strip>;
|
|
488
|
+
finishReason: z.ZodString;
|
|
487
489
|
usage: z.ZodOptional<z.ZodObject<{
|
|
488
490
|
inputTokens: z.ZodOptional<z.ZodNumber>;
|
|
489
491
|
outputTokens: z.ZodOptional<z.ZodNumber>;
|
|
@@ -70,6 +70,7 @@ export const ToolFinishedEventSchema = z.object({
|
|
|
70
70
|
export const FinishEventSchema = z.object({
|
|
71
71
|
type: z.literal("finish"),
|
|
72
72
|
source: EventSourceSchema,
|
|
73
|
+
finishReason: z.string(),
|
|
73
74
|
usage: TokenUsageRollupSchema.optional(),
|
|
74
75
|
turnUsage: TokenUsageRollupSchema.optional(),
|
|
75
76
|
});
|
|
@@ -625,6 +625,25 @@ export declare const PausePartSchema: z.ZodObject<{
|
|
|
625
625
|
attempt: z.ZodOptional<z.ZodNumber>;
|
|
626
626
|
retryAt: z.ZodOptional<z.ZodString>;
|
|
627
627
|
}, z.core.$strip>;
|
|
628
|
+
/** Mirrors `ModelRefusalPart` without its `type`: the stored payload of the part. */
|
|
629
|
+
export declare const ModelRefusalPayloadSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
630
|
+
id: z.ZodString;
|
|
631
|
+
outcome: z.ZodLiteral<"fallback">;
|
|
632
|
+
model: z.ZodString;
|
|
633
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
634
|
+
id: z.ZodString;
|
|
635
|
+
outcome: z.ZodLiteral<"stopped">;
|
|
636
|
+
}, z.core.$strip>], "outcome">;
|
|
637
|
+
export declare const ModelRefusalPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
638
|
+
id: z.ZodString;
|
|
639
|
+
outcome: z.ZodLiteral<"fallback">;
|
|
640
|
+
model: z.ZodString;
|
|
641
|
+
type: z.ZodLiteral<"data-model-refusal">;
|
|
642
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
643
|
+
id: z.ZodString;
|
|
644
|
+
outcome: z.ZodLiteral<"stopped">;
|
|
645
|
+
type: z.ZodLiteral<"data-model-refusal">;
|
|
646
|
+
}, z.core.$strip>], "outcome">;
|
|
628
647
|
export declare const ModelRetryPartSchema: z.ZodObject<{
|
|
629
648
|
type: z.ZodLiteral<"data-model-retry">;
|
|
630
649
|
id: z.ZodString;
|
|
@@ -1090,4 +1109,13 @@ export declare const ChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
1090
1109
|
maxRetries: z.ZodNumber;
|
|
1091
1110
|
retryAt: z.ZodString;
|
|
1092
1111
|
status: z.ZodOptional<z.ZodNumber>;
|
|
1093
|
-
}, z.core.$strip
|
|
1112
|
+
}, z.core.$strip>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1113
|
+
id: z.ZodString;
|
|
1114
|
+
outcome: z.ZodLiteral<"fallback">;
|
|
1115
|
+
model: z.ZodString;
|
|
1116
|
+
type: z.ZodLiteral<"data-model-refusal">;
|
|
1117
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
1118
|
+
id: z.ZodString;
|
|
1119
|
+
outcome: z.ZodLiteral<"stopped">;
|
|
1120
|
+
type: z.ZodLiteral<"data-model-refusal">;
|
|
1121
|
+
}, z.core.$strip>], "outcome">], "type">;
|
|
@@ -375,6 +375,15 @@ export const PausePartSchema = z.object({
|
|
|
375
375
|
attempt: z.number().int().nonnegative().optional(),
|
|
376
376
|
retryAt: z.string().optional(),
|
|
377
377
|
});
|
|
378
|
+
// ── Model refusal ───────────────────────────────────────────────────
|
|
379
|
+
const ModelRefusalFallbackShape = { id: z.string(), outcome: z.literal("fallback"), model: z.string() };
|
|
380
|
+
const ModelRefusalStoppedShape = { id: z.string(), outcome: z.literal("stopped") };
|
|
381
|
+
/** Mirrors `ModelRefusalPart` without its `type`: the stored payload of the part. */
|
|
382
|
+
export const ModelRefusalPayloadSchema = z.discriminatedUnion("outcome", [z.object(ModelRefusalFallbackShape), z.object(ModelRefusalStoppedShape)]);
|
|
383
|
+
export const ModelRefusalPartSchema = z.discriminatedUnion("outcome", [
|
|
384
|
+
z.object({ type: z.literal("data-model-refusal"), ...ModelRefusalFallbackShape }),
|
|
385
|
+
z.object({ type: z.literal("data-model-refusal"), ...ModelRefusalStoppedShape }),
|
|
386
|
+
]);
|
|
378
387
|
// ── Model retry ─────────────────────────────────────────────────────
|
|
379
388
|
export const ModelRetryPartSchema = z.object({
|
|
380
389
|
type: z.literal("data-model-retry"),
|
|
@@ -428,4 +437,5 @@ export const ChatPartSchema = z.discriminatedUnion("type", [
|
|
|
428
437
|
CompactionPartSchema,
|
|
429
438
|
PausePartSchema,
|
|
430
439
|
ModelRetryPartSchema,
|
|
440
|
+
ModelRefusalPartSchema,
|
|
431
441
|
]);
|
package/dist/index.d.ts
CHANGED
|
@@ -108,7 +108,7 @@ export type { SubAgentActivityTracker } from "./contracts/subagent-activity.js";
|
|
|
108
108
|
export type { TextPart, ToolCallPart, MessagePart, ChatMessage } from "./contracts/message.js";
|
|
109
109
|
export type { TurnTerminal, ApplyFrameResult, ChatPartCheck } from "./contracts/chat-frame.js";
|
|
110
110
|
export type { TokenUsageRollup } from "./contracts/usage.js";
|
|
111
|
-
export type { ChatPart, PresentationPart, PresentationContent, PlanPart, RunCardPart, PlanApprovalPart, FileReferencePart, FileReferenceEntry, RunStartedPart, DagStatePart, DagStepState, PlanGraphStep, StepStatus, StepActivityPart, ActivityEntryPart, SubAgentActivityEntry, SubAgentActivityPart, StepPhase, StepFileTreePart, FileTreeEntry, StepOutputPart, StepOutputFile, StepSummaryPart, StepUsagePart, StepBlockedPart, RunSynthesisPart, FindingReference, SynthesizedFinding, BiologicalTheme, SynthesisProgressPart, SynthesisPhase, RunCompletedPart, RunCompletedFinding, RunFailedPart, CompactionPart, PauseCause, WorkKind, PausePart, ModelRetryPart, AskPart, ChildSessionStartedPart, ReportRenderedPart, } from "./contracts/chat-parts.js";
|
|
111
|
+
export type { ChatPart, PresentationPart, PresentationContent, PlanPart, RunCardPart, PlanApprovalPart, FileReferencePart, FileReferenceEntry, RunStartedPart, DagStatePart, DagStepState, PlanGraphStep, StepStatus, StepActivityPart, ActivityEntryPart, SubAgentActivityEntry, SubAgentActivityPart, StepPhase, StepFileTreePart, FileTreeEntry, StepOutputPart, StepOutputFile, StepSummaryPart, StepUsagePart, StepBlockedPart, RunSynthesisPart, FindingReference, SynthesizedFinding, BiologicalTheme, SynthesisProgressPart, SynthesisPhase, RunCompletedPart, RunCompletedFinding, RunFailedPart, CompactionPart, PauseCause, WorkKind, PausePart, ModelRetryPart, ModelRefusal, ModelRefusalPart, AskPart, ChildSessionStartedPart, ReportRenderedPart, } from "./contracts/chat-parts.js";
|
|
112
112
|
export { PART_REGISTRY, isTransient, isReconciling, isSidebarPart } from "./contracts/part-registry.js";
|
|
113
113
|
export type { ChatPartType, PartDescriptor, PartEmitter, PartConsumer } from "./contracts/part-registry.js";
|
|
114
114
|
export { parseStructureUrl, alphafoldEntryUrl, alphafoldPredictionUrl } from "./contracts/structure-source.js";
|
package/dist/loop/chat-stream.js
CHANGED
|
@@ -156,7 +156,7 @@ export function framesOfChunk(chunk, rootSource) {
|
|
|
156
156
|
case "error":
|
|
157
157
|
return [{ type: "error", message: chunk.errorText, source: rootSource }];
|
|
158
158
|
case "abort":
|
|
159
|
-
return [{ type: "finish", source: rootSource }];
|
|
159
|
+
return [{ type: "finish", source: rootSource, finishReason: "aborted" }];
|
|
160
160
|
default: {
|
|
161
161
|
const frame = harnessFrameOf(chunk);
|
|
162
162
|
return frame === undefined ? [] : [frame];
|
package/dist/loop/run-loop.js
CHANGED
|
@@ -654,6 +654,9 @@ async function runSeries(agent, opts, calls, log, target, spec) {
|
|
|
654
654
|
target.steps.push(loopStep);
|
|
655
655
|
if (loopStep.calls.length > 0)
|
|
656
656
|
log.debug("iteration", { iteration: seriesSteps.length - 1, tools: loopStep.calls.map((call) => call.toolName) });
|
|
657
|
+
const refusal = refusalOf(step, loopStep);
|
|
658
|
+
if (refusal !== undefined)
|
|
659
|
+
await opts.emit({ type: "data-model-refusal", source, data: refusal });
|
|
657
660
|
await opts.emit({ type: "iteration", source, index: seriesSteps.length - 1, final: !continuesAfter(loopStep) || stops() });
|
|
658
661
|
},
|
|
659
662
|
});
|
|
@@ -749,6 +752,19 @@ function loopStepOf(step, deniedByTheUser) {
|
|
|
749
752
|
});
|
|
750
753
|
return { finishReason: step.finishReason, rawFinishReason: step.rawFinishReason, calls };
|
|
751
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* The refusal of one step, when the provider declined its request. A reply that ends with the content filter
|
|
757
|
+
* stopped, also after a fallback hop, because a chain whose last model declined reports that hop too. A hop that
|
|
758
|
+
* names no model gives the served model of the response, as the usage record does.
|
|
759
|
+
*/
|
|
760
|
+
function refusalOf(step, loopStep) {
|
|
761
|
+
if (loopStep.finishReason === "content-filter")
|
|
762
|
+
return { id: step.response.id, outcome: "stopped" };
|
|
763
|
+
const servedBy = fallbackModelOf(step.providerMetadata);
|
|
764
|
+
if (servedBy === undefined)
|
|
765
|
+
return undefined;
|
|
766
|
+
return { id: step.response.id, outcome: "fallback", model: servedBy === "" ? step.response.modelId : servedBy };
|
|
767
|
+
}
|
|
752
768
|
/**
|
|
753
769
|
* The messages of one step as the transcript records them. The request after
|
|
754
770
|
* them sends them as they are recorded, thus the record is the prefix of the
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* reconstruction.
|
|
16
16
|
*/
|
|
17
17
|
import { type DataUIPart, type UIMessage } from "ai";
|
|
18
|
-
import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
|
|
18
|
+
import type { AskPart, ChildSessionStartedPart, CompactionPart, FileReferencePart, ModelRefusal, ModelRefusalPart, PlanApprovalPart, PlanPart, PresentationPart, ReportRenderedPart, RunCardPart, SubAgentActivityEntry, SubAgentActivityPart } from "../contracts/chat-parts.js";
|
|
19
19
|
import type { ToolCallOutcome } from "../contracts/chat-events.js";
|
|
20
20
|
import type { ChatMessage, MessagePart, ToolCallPart } from "../contracts/message.js";
|
|
21
21
|
import type { Logger } from "../lib/logger.js";
|
|
@@ -43,6 +43,7 @@ export type ConversationUIData = {
|
|
|
43
43
|
"child-session-started": Payload<ChildSessionStartedPart>;
|
|
44
44
|
"report-rendered": Payload<ReportRenderedPart>;
|
|
45
45
|
compaction: Payload<CompactionPart>;
|
|
46
|
+
"model-refusal": ModelRefusal;
|
|
46
47
|
"subagent-activity": Omit<Payload<SubAgentActivityPart>, "entries"> & {
|
|
47
48
|
entries: (Omit<SubAgentActivityEntry, "outcome"> & {
|
|
48
49
|
outcome: ToolCallOutcome;
|
|
@@ -65,7 +66,7 @@ export declare function parseStoredDisplayEnvelope(value: unknown, identity: str
|
|
|
65
66
|
export type StoredSourcePart = Extract<Exclude<MessagePart, {
|
|
66
67
|
type: "text";
|
|
67
68
|
}>, {
|
|
68
|
-
type: ToolCallPart["type"] | PresentationPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | SubAgentActivityPart["type"];
|
|
69
|
+
type: ToolCallPart["type"] | PresentationPart["type"] | PlanPart["type"] | RunCardPart["type"] | PlanApprovalPart["type"] | FileReferencePart["type"] | AskPart["type"] | ChildSessionStartedPart["type"] | ReportRenderedPart["type"] | CompactionPart["type"] | ModelRefusalPart["type"] | SubAgentActivityPart["type"];
|
|
69
70
|
}>;
|
|
70
71
|
/** Whether `part` is an arm the stored vocabulary can hold; anything else cannot become a stored card. */
|
|
71
72
|
export declare function isStoredSourcePart(part: Exclude<MessagePart, {
|
|
@@ -19,7 +19,7 @@ import { textOf } from "../lib/unknown-text.js";
|
|
|
19
19
|
import { isDataUIPart, validateUIMessages } from "ai";
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { parseUnknownRecord } from "../lib/unknown-containers.js";
|
|
22
|
-
import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
22
|
+
import { AskPartSchema, ChildSessionStartedPartSchema, CompactionPartSchema, FileReferencePartSchema, ModelRefusalPayloadSchema, PlanApprovalPartSchema, PlanPartSchema, PresentationPartSchema, ReportRenderedPartSchema, RunCardPartSchema, SubAgentActivityEntrySchema, SubAgentActivityPartSchema, } from "../contracts/schemas/chat-parts.js";
|
|
23
23
|
import { ToolCallOutcomeSchema, ToolCallValueSchema } from "../contracts/schemas/chat-events.js";
|
|
24
24
|
export const SUPPORTED_DISPLAY_SCHEMA_VERSION = 1;
|
|
25
25
|
const DisplayEnvelopeHeaderSchema = z.object({
|
|
@@ -77,6 +77,7 @@ const dataSchemas = {
|
|
|
77
77
|
"child-session-started": ChildSessionStartedPartSchema.omit({ type: true }),
|
|
78
78
|
"report-rendered": ReportRenderedPartSchema.omit({ type: true }),
|
|
79
79
|
compaction: CompactionPartSchema.omit({ type: true }),
|
|
80
|
+
"model-refusal": ModelRefusalPayloadSchema,
|
|
80
81
|
"subagent-activity": SubAgentActivityPartSchema.omit({ type: true }).extend({
|
|
81
82
|
entries: z.array(SubAgentActivityEntrySchema.extend({ outcome: ToolCallOutcomeSchema })),
|
|
82
83
|
}),
|
|
@@ -182,6 +183,7 @@ const STORED_SOURCE_TYPES = [
|
|
|
182
183
|
"data-child-session-started",
|
|
183
184
|
"data-report-rendered",
|
|
184
185
|
"data-compaction",
|
|
186
|
+
"data-model-refusal",
|
|
185
187
|
"data-subagent-activity",
|
|
186
188
|
];
|
|
187
189
|
const STORED_SOURCE_TYPE_SET = new Set(STORED_SOURCE_TYPES);
|
|
@@ -255,6 +257,10 @@ export function conversationDisplayPart(part) {
|
|
|
255
257
|
const { type: _type, ...data } = part;
|
|
256
258
|
return { type: "data-compaction", id, data };
|
|
257
259
|
}
|
|
260
|
+
case "data-model-refusal": {
|
|
261
|
+
const { type: _type, ...data } = part;
|
|
262
|
+
return { type: "data-model-refusal", id, data };
|
|
263
|
+
}
|
|
258
264
|
case "data-subagent-activity": {
|
|
259
265
|
const { type: _type, ...payload } = part;
|
|
260
266
|
return { type: "data-subagent-activity", id, data: { ...payload, entries: part.entries.map(closedEntry) } };
|
|
@@ -315,6 +321,9 @@ export function conversationUIToChatMessages(messages) {
|
|
|
315
321
|
case "data-compaction":
|
|
316
322
|
parts.push({ type: "data-compaction", ...part.data });
|
|
317
323
|
break;
|
|
324
|
+
case "data-model-refusal":
|
|
325
|
+
parts.push({ type: "data-model-refusal", ...part.data });
|
|
326
|
+
break;
|
|
318
327
|
case "data-subagent-activity":
|
|
319
328
|
parts.push({ type: "data-subagent-activity", ...part.data });
|
|
320
329
|
break;
|
|
@@ -118,6 +118,12 @@ export type AiSdkProviderConfig = {
|
|
|
118
118
|
* Needs the `thinking-binding-controls-2026-08-01` beta header.
|
|
119
119
|
*/
|
|
120
120
|
readonly thinkingBinding?: "drop_block" | "error" | "off";
|
|
121
|
+
/**
|
|
122
|
+
* `default`: when a safety classifier declines a request, the API sends it again to the model that it
|
|
123
|
+
* recommends for the category of the refusal, inside the same call. When the field is absent, a declined
|
|
124
|
+
* request ends with the content filter. A model with no fallback target keeps its refusal.
|
|
125
|
+
*/
|
|
126
|
+
readonly fallbacks?: "default";
|
|
121
127
|
} | {
|
|
122
128
|
readonly kind: "openai";
|
|
123
129
|
readonly baseURL?: string;
|
|
@@ -252,22 +258,19 @@ export declare function sessionKeyDigestOf(session: Pick<AgentSession, "scope" |
|
|
|
252
258
|
* A model can refuse a request by category — a biology prompt is one such category —
|
|
253
259
|
* and the API then re-serves it on another model. It reports the hop TWICE: as a
|
|
254
260
|
* `fallback` content block inside the assistant turn, and on
|
|
255
|
-
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
261
|
+
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
|
|
262
|
+
* custom part and sends it back on the next request, but it keeps each thinking block
|
|
263
|
+
* before it too. The API accepts no thinking block from before the hop, and a turn
|
|
264
|
+
* whose thinking blocks changed earns `thinking ... blocks in the latest assistant
|
|
265
|
+
* message cannot be modified`, which is a 400 that no retry can pass.
|
|
260
266
|
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
* way back, thus the loop records the reply of a hop with no thinking block, and the next
|
|
265
|
-
* request continues. The cost is the reasoning of this one turn and the cache write that
|
|
266
|
-
* its new prefix pays for.
|
|
267
|
+
* A thinking block is optional on the way back, thus the loop records the reply of a
|
|
268
|
+
* hop with no thinking block, and the next request continues. The cost is the
|
|
269
|
+
* reasoning of this one turn and the cache write that its new prefix pays for.
|
|
267
270
|
*
|
|
268
|
-
* The response metadata of the package carries `message_start.message.model`, and
|
|
269
|
-
*
|
|
270
|
-
* answered, thus it wins for the usage record.
|
|
271
|
+
* The response metadata of the package carries `message_start.message.model`, and
|
|
272
|
+
* after a hop in the middle of a reply that field keeps the REQUESTED model. The
|
|
273
|
+
* iteration names the model that answered, thus it wins for the usage record.
|
|
271
274
|
*/
|
|
272
275
|
export declare function fallbackModelOf(metadata: ProviderMetadata | undefined): string | undefined;
|
|
273
276
|
/**
|
package/dist/providers/ai-sdk.js
CHANGED
|
@@ -108,22 +108,19 @@ function thinkingWithBinding(reasoning, mode) {
|
|
|
108
108
|
* A model can refuse a request by category — a biology prompt is one such category —
|
|
109
109
|
* and the API then re-serves it on another model. It reports the hop TWICE: as a
|
|
110
110
|
* `fallback` content block inside the assistant turn, and on
|
|
111
|
-
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
111
|
+
* `providerMetadata.anthropic.iterations`. `@ai-sdk/anthropic` keeps the block as a
|
|
112
|
+
* custom part and sends it back on the next request, but it keeps each thinking block
|
|
113
|
+
* before it too. The API accepts no thinking block from before the hop, and a turn
|
|
114
|
+
* whose thinking blocks changed earns `thinking ... blocks in the latest assistant
|
|
115
|
+
* message cannot be modified`, which is a 400 that no retry can pass.
|
|
116
116
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* way back, thus the loop records the reply of a hop with no thinking block, and the next
|
|
121
|
-
* request continues. The cost is the reasoning of this one turn and the cache write that
|
|
122
|
-
* its new prefix pays for.
|
|
117
|
+
* A thinking block is optional on the way back, thus the loop records the reply of a
|
|
118
|
+
* hop with no thinking block, and the next request continues. The cost is the
|
|
119
|
+
* reasoning of this one turn and the cache write that its new prefix pays for.
|
|
123
120
|
*
|
|
124
|
-
* The response metadata of the package carries `message_start.message.model`, and
|
|
125
|
-
*
|
|
126
|
-
* answered, thus it wins for the usage record.
|
|
121
|
+
* The response metadata of the package carries `message_start.message.model`, and
|
|
122
|
+
* after a hop in the middle of a reply that field keeps the REQUESTED model. The
|
|
123
|
+
* iteration names the model that answered, thus it wins for the usage record.
|
|
127
124
|
*/
|
|
128
125
|
export function fallbackModelOf(metadata) {
|
|
129
126
|
const entries = metadata?.["anthropic"]?.["iterations"];
|
|
@@ -717,12 +714,13 @@ export function createConfiguredAiSdkProvider(deps) {
|
|
|
717
714
|
maxRetries: config.maxRetries,
|
|
718
715
|
requestTimeoutMs,
|
|
719
716
|
reasoning: config.reasoning,
|
|
720
|
-
// The package sends the key as `metadata.user_id`,
|
|
721
|
-
// `thinking.block_binding
|
|
717
|
+
// The package sends the key as `metadata.user_id`, the binding as
|
|
718
|
+
// `thinking.block_binding`, and the default fallback with its beta header.
|
|
722
719
|
providerOptionsFor: ({ session, reasoning }) => ({
|
|
723
720
|
anthropic: {
|
|
724
721
|
metadata: { userId: sessionKeyDigestOf(session) },
|
|
725
722
|
thinking: binding === undefined ? undefined : thinkingWithBinding(reasoning, binding),
|
|
723
|
+
fallbacks: config.fallbacks,
|
|
726
724
|
},
|
|
727
725
|
}),
|
|
728
726
|
});
|
package/package.json
CHANGED