chainlesschain 0.166.131 → 0.166.133
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 +46 -18
- package/package.json +1 -1
- package/src/assets/linux-subreaper/linux-arm64/manifest.json +1 -1
- package/src/assets/linux-subreaper/linux-x64/manifest.json +1 -1
- package/src/data/changelog.json +26 -0
- package/src/gateways/ws/llm-chat-protocol.js +1 -1
- package/src/gateways/ws/personal-data-hub-protocol.js +27 -11
- package/src/gateways/ws/session-core-protocol.js +22 -17
- package/src/gateways/ws/session-protocol.js +19 -8
- package/src/gateways/ws/ws-agent-handler.js +30 -0
- package/src/lib/chat-core.js +40 -35
- package/src/lib/context-memory-kernel/message-adapter.js +27 -6
- package/src/lib/context-memory-kernel/provider-context.js +48 -3
- package/src/lib/evolution/agent-model-projection.js +0 -1
- package/src/lib/evolution/governed-hub-llm.js +5 -1
- package/src/lib/evolution/governed-hub-resolver.js +15 -6
- package/src/lib/evolution/governed-hub-skill.js +2 -0
- package/src/lib/evolution/optional-ui-model-turn.js +13 -2
- package/src/lib/llm-config-defaults.js +17 -5
- package/src/lib/llm-connection-config.js +10 -2
- package/src/lib/llm-connection-probe.js +5 -2
- package/src/lib/llm-connection-resolution.js +7 -4
- package/src/lib/llm-providers.js +24 -11
- package/src/lib/openai-responses.js +2 -1
- package/src/lib/personal-data-hub-wiring.js +69 -36
- package/src/lib/provider-protocol.js +35 -3
- package/src/lib/provider-stream.js +6 -4
- package/src/lib/runnable-provider.js +4 -0
- package/src/lib/ws-chat-handler.js +22 -9
- package/src/runtime/agent-core.js +201 -15
- package/src/runtime/message-roles.js +67 -3
- package/src/runtime/provider-stream-state.js +35 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectMessagesWithTaskBinding } from "./message-roles.js";
|
|
1
2
|
import {
|
|
2
3
|
providerFetch,
|
|
3
4
|
requireUnscopedToolExecution,
|
|
@@ -9,7 +10,10 @@ import {
|
|
|
9
10
|
normalizeProviderBaseUrl,
|
|
10
11
|
openAICompatibleBaseUrl,
|
|
11
12
|
} from "../lib/provider-protocol.js";
|
|
12
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
BUILT_IN_PROVIDERS,
|
|
15
|
+
resolveProviderDefinition,
|
|
16
|
+
} from "../lib/llm-providers.js";
|
|
13
17
|
import { resolveLlmConnection } from "../lib/llm-connection-resolution.js";
|
|
14
18
|
/**
|
|
15
19
|
* Agent Core — transport-independent agentic logic
|
|
@@ -28,6 +32,7 @@ import { resolveLlmConnection } from "../lib/llm-connection-resolution.js";
|
|
|
28
32
|
*/
|
|
29
33
|
|
|
30
34
|
import fs from "fs";
|
|
35
|
+
import { isDeepStrictEqual } from "node:util";
|
|
31
36
|
import { isUtf8 } from "node:buffer";
|
|
32
37
|
import {
|
|
33
38
|
_isNonNegativeSafeInteger,
|
|
@@ -272,6 +277,8 @@ import { isMcpRpcError } from "../harness/mcp-client.js";
|
|
|
272
277
|
import {
|
|
273
278
|
projectCanonicalResumeMessages,
|
|
274
279
|
markRuntimeOnlySessionMessage,
|
|
280
|
+
isRuntimeOnlySessionMessage,
|
|
281
|
+
encodePersistedMessage,
|
|
275
282
|
} from "../lib/session-message-provenance.js";
|
|
276
283
|
import { releaseOldLiveSessionResults } from "../lib/session-runtime-retention.js";
|
|
277
284
|
import {
|
|
@@ -12118,11 +12125,65 @@ function getEffectiveToolDefinitions(options = {}) {
|
|
|
12118
12125
|
});
|
|
12119
12126
|
}
|
|
12120
12127
|
|
|
12128
|
+
function projectExactFileMutationToolSchemas(
|
|
12129
|
+
definitions,
|
|
12130
|
+
scope,
|
|
12131
|
+
externalToolExecutors,
|
|
12132
|
+
) {
|
|
12133
|
+
if (scope == null) return definitions;
|
|
12134
|
+
return definitions.map((tool) => {
|
|
12135
|
+
const name = tool?.function?.name;
|
|
12136
|
+
if (
|
|
12137
|
+
!["write_file", "edit_file", "edit_file_hashed"].includes(name) ||
|
|
12138
|
+
!AGENT_TOOLS.includes(tool) ||
|
|
12139
|
+
externalToolExecutors?.[name]
|
|
12140
|
+
) {
|
|
12141
|
+
return tool;
|
|
12142
|
+
}
|
|
12143
|
+
const parameters = tool.function.parameters;
|
|
12144
|
+
return {
|
|
12145
|
+
...tool,
|
|
12146
|
+
function: {
|
|
12147
|
+
...tool.function,
|
|
12148
|
+
parameters: {
|
|
12149
|
+
...parameters,
|
|
12150
|
+
properties: {
|
|
12151
|
+
...parameters.properties,
|
|
12152
|
+
path: {
|
|
12153
|
+
...parameters.properties.path,
|
|
12154
|
+
// Complete host-authorized set: never truncate this enum or
|
|
12155
|
+
// derive it from a read result. Normal tool context budgeting
|
|
12156
|
+
// sees the same schema that is sent to the provider.
|
|
12157
|
+
enum: [...scope.allowedPaths],
|
|
12158
|
+
description:
|
|
12159
|
+
"Use exactly one listed canonical repo-relative path. Read tools may return absolute paths; those are not valid mutation arguments in this run. Do not use an absolute path, ./ prefix, .. segment or backslash. This schema is guidance; all execution-time permission and file-identity checks still apply.",
|
|
12160
|
+
},
|
|
12161
|
+
},
|
|
12162
|
+
},
|
|
12163
|
+
},
|
|
12164
|
+
};
|
|
12165
|
+
});
|
|
12166
|
+
}
|
|
12167
|
+
|
|
12121
12168
|
function getProviderToolDefinitions(options = {}) {
|
|
12122
|
-
|
|
12169
|
+
const definitions = projectMcpToolDefinitions(
|
|
12123
12170
|
getEffectiveToolDefinitions(options),
|
|
12124
12171
|
options.externalToolExecutors,
|
|
12125
12172
|
);
|
|
12173
|
+
if (options.fileMutationScope == null) return definitions;
|
|
12174
|
+
if (options.exactToolNames !== true || options.hermeticExecution !== true) {
|
|
12175
|
+
throw new Error(
|
|
12176
|
+
"exact file mutation scope requires exactToolNames=true and hermeticExecution=true",
|
|
12177
|
+
);
|
|
12178
|
+
}
|
|
12179
|
+
const scope = normalizeExactFileMutationScope(options.fileMutationScope, {
|
|
12180
|
+
cwd: options.cwd || process.cwd(),
|
|
12181
|
+
});
|
|
12182
|
+
return projectExactFileMutationToolSchemas(
|
|
12183
|
+
definitions,
|
|
12184
|
+
scope,
|
|
12185
|
+
options.externalToolExecutors,
|
|
12186
|
+
);
|
|
12126
12187
|
}
|
|
12127
12188
|
|
|
12128
12189
|
const PROVIDER_REQUEST_ID_RE = /^[\x21-\x7e]{1,512}$/;
|
|
@@ -12536,7 +12597,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
12536
12597
|
}
|
|
12537
12598
|
|
|
12538
12599
|
// OpenAI-compatible providers
|
|
12539
|
-
const providerDefinition =
|
|
12600
|
+
const providerDefinition = resolveProviderDefinition(provider);
|
|
12540
12601
|
const url = openAICompatibleBaseUrl(provider, baseUrl);
|
|
12541
12602
|
|
|
12542
12603
|
if (!providerDefinition || !url) {
|
|
@@ -12551,7 +12612,8 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
12551
12612
|
|
|
12552
12613
|
const envKey = providerDefinition.apiKeyEnv;
|
|
12553
12614
|
const key = apiKey;
|
|
12554
|
-
if (!key
|
|
12615
|
+
if (!key && provider !== "custom")
|
|
12616
|
+
throw new Error(`${envKey} required for provider ${provider}`);
|
|
12555
12617
|
const effectiveModel = model || providerDefinition.models?.[0];
|
|
12556
12618
|
const modelProfile = resolveModelCapabilityProfile({
|
|
12557
12619
|
provider,
|
|
@@ -12691,7 +12753,7 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
12691
12753
|
method: "POST",
|
|
12692
12754
|
headers: {
|
|
12693
12755
|
"Content-Type": "application/json",
|
|
12694
|
-
Authorization: `Bearer ${key}
|
|
12756
|
+
...(key ? { Authorization: `Bearer ${key}` } : {}),
|
|
12695
12757
|
...(supportsOpenAIRequestIdentity && providerRequestId
|
|
12696
12758
|
? { "X-Client-Request-Id": providerRequestId }
|
|
12697
12759
|
: {}),
|
|
@@ -12748,6 +12810,10 @@ export async function chatWithTools(rawMessages, options) {
|
|
|
12748
12810
|
message.tool_calls = providerMessage.tool_calls;
|
|
12749
12811
|
}
|
|
12750
12812
|
const out = { message };
|
|
12813
|
+
if (choice.finish_reason != null) {
|
|
12814
|
+
out.finishReason = choice.finish_reason;
|
|
12815
|
+
out.providerTerminationComplete = data.choices.length === 1;
|
|
12816
|
+
}
|
|
12751
12817
|
const providerReceipt = supportsOpenAIRequestIdentity
|
|
12752
12818
|
? _openAIProviderRequestReceipt(response, data, providerRequestId)
|
|
12753
12819
|
: null;
|
|
@@ -13307,7 +13373,7 @@ async function _chatOpenAIStreaming(
|
|
|
13307
13373
|
method: "POST",
|
|
13308
13374
|
headers: {
|
|
13309
13375
|
"Content-Type": "application/json",
|
|
13310
|
-
Authorization: `Bearer ${apiKey}
|
|
13376
|
+
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
13311
13377
|
...(providerRequestId
|
|
13312
13378
|
? { "X-Client-Request-Id": providerRequestId }
|
|
13313
13379
|
: {}),
|
|
@@ -14800,6 +14866,30 @@ function permissionDecision(callId, tool, result) {
|
|
|
14800
14866
|
}
|
|
14801
14867
|
|
|
14802
14868
|
export async function* agentLoop(messages, options) {
|
|
14869
|
+
// Bind the actual task before compaction, runtime nudges or provider projection.
|
|
14870
|
+
// A new host-admitted user turn starts a new loop and receives its own binding.
|
|
14871
|
+
const currentUserTask = messages.findLast(
|
|
14872
|
+
(message) =>
|
|
14873
|
+
message?.role === "user" && !isRuntimeOnlySessionMessage(message),
|
|
14874
|
+
);
|
|
14875
|
+
const currentUserTaskSnapshot = currentUserTask
|
|
14876
|
+
? encodePersistedMessage(currentUserTask)
|
|
14877
|
+
: null;
|
|
14878
|
+
const currentUserTaskBinding = currentUserTaskSnapshot
|
|
14879
|
+
? {
|
|
14880
|
+
message: currentUserTaskSnapshot,
|
|
14881
|
+
original: currentUserTask,
|
|
14882
|
+
allowContentRelocation:
|
|
14883
|
+
messages.filter(
|
|
14884
|
+
(message) =>
|
|
14885
|
+
message?.role === "user" &&
|
|
14886
|
+
isDeepStrictEqual(
|
|
14887
|
+
encodePersistedMessage(message),
|
|
14888
|
+
currentUserTaskSnapshot,
|
|
14889
|
+
),
|
|
14890
|
+
).length === 1,
|
|
14891
|
+
}
|
|
14892
|
+
: null;
|
|
14803
14893
|
if (options.serviceTier != null) {
|
|
14804
14894
|
resolveModelServiceTier(
|
|
14805
14895
|
resolveModelCapabilityProfile(options),
|
|
@@ -14986,6 +15076,7 @@ export async function* agentLoop(messages, options) {
|
|
|
14986
15076
|
let effectiveToolOptions = hermeticExecution
|
|
14987
15077
|
? {
|
|
14988
15078
|
...options,
|
|
15079
|
+
fileMutationScope,
|
|
14989
15080
|
hostManagedToolPolicy: null,
|
|
14990
15081
|
hostManagedToolPolicyAuthority: null,
|
|
14991
15082
|
extraToolDefinitions: [],
|
|
@@ -15384,6 +15475,8 @@ export async function* agentLoop(messages, options) {
|
|
|
15384
15475
|
// one-shot guard means a model that keeps returning empty turns completes
|
|
15385
15476
|
// rather than looping forever (the iteration budget is the hard backstop).
|
|
15386
15477
|
let emptyThinkingReprompted = false;
|
|
15478
|
+
let outputLimitRecoveryUsed = false;
|
|
15479
|
+
let outputLimitRecoveryPending = null;
|
|
15387
15480
|
let readFileLoopGuard = new ReadFileLoopGuard();
|
|
15388
15481
|
let remoteReadLoopGuard = new RemoteReadLoopGuard();
|
|
15389
15482
|
const prActionTask = isPrActionRequest(messages);
|
|
@@ -16316,27 +16409,43 @@ export async function* agentLoop(messages, options) {
|
|
|
16316
16409
|
index === resultIndex ? { ...result, content } : message,
|
|
16317
16410
|
);
|
|
16318
16411
|
} else {
|
|
16319
|
-
// Full compaction can leave only the pinned request.
|
|
16320
|
-
//
|
|
16321
|
-
const
|
|
16412
|
+
// Full compaction can leave only the pinned request. Move that same
|
|
16413
|
+
// message after the source data, preserving its metadata and identity.
|
|
16414
|
+
const requestIndex = requestMessages.findLastIndex(
|
|
16322
16415
|
(message) => message.role === "user",
|
|
16323
16416
|
);
|
|
16417
|
+
const request = requestMessages[requestIndex];
|
|
16324
16418
|
callMessages = [
|
|
16325
|
-
...requestMessages,
|
|
16419
|
+
...requestMessages.filter((_, index) => index !== requestIndex),
|
|
16326
16420
|
...readContext.map((content) => ({ role: "assistant", content })),
|
|
16327
|
-
|
|
16328
|
-
role: "user",
|
|
16329
|
-
content: request?.content || "Continue the original user task.",
|
|
16330
|
-
},
|
|
16421
|
+
...(request ? [request] : []),
|
|
16331
16422
|
];
|
|
16332
16423
|
}
|
|
16333
16424
|
}
|
|
16334
16425
|
// Role normalization is request-only: durable history remains unchanged
|
|
16335
16426
|
// for tool settlement and canonical compaction authority comparisons.
|
|
16427
|
+
let projectedUserTaskBinding = currentUserTaskBinding;
|
|
16336
16428
|
if (typeof options.projectProviderMessages === "function") {
|
|
16337
|
-
|
|
16429
|
+
const projected = projectMessagesWithTaskBinding(
|
|
16430
|
+
options.projectProviderMessages,
|
|
16431
|
+
callMessages,
|
|
16432
|
+
currentUserTaskBinding,
|
|
16433
|
+
);
|
|
16434
|
+
callMessages = projected.messages;
|
|
16435
|
+
projectedUserTaskBinding = projected.taskBinding;
|
|
16338
16436
|
}
|
|
16339
16437
|
const contextMemoryTrustedSystemIndexes = [];
|
|
16438
|
+
if (outputLimitRecoveryPending !== null) {
|
|
16439
|
+
// A synthetic user turn would replace the protected real task under
|
|
16440
|
+
// canonical context pressure. Keep this one-request hint out of history
|
|
16441
|
+
// and explicitly admit it as trusted system policy in the next plan.
|
|
16442
|
+
callMessages = [
|
|
16443
|
+
...callMessages,
|
|
16444
|
+
{ role: "system", content: outputLimitRecoveryPending },
|
|
16445
|
+
];
|
|
16446
|
+
contextMemoryTrustedSystemIndexes.push(callMessages.length - 1);
|
|
16447
|
+
outputLimitRecoveryPending = null;
|
|
16448
|
+
}
|
|
16340
16449
|
if (Number.isFinite(budget.limit)) {
|
|
16341
16450
|
// consume() already charged this dispatch. The current request may still
|
|
16342
16451
|
// act even when no further provider request remains. Rebuild this hint
|
|
@@ -16496,6 +16605,11 @@ export async function* agentLoop(messages, options) {
|
|
|
16496
16605
|
contextMemoryToolDefinitions:
|
|
16497
16606
|
getProviderToolDefinitions(iterationToolOptions),
|
|
16498
16607
|
contextMemoryTrustedSystemIndexes,
|
|
16608
|
+
// Independent review uses its existing merged requirement journal and
|
|
16609
|
+
// validates every original requirement after planning below.
|
|
16610
|
+
contextMemoryRequiredUserBinding: independentReviewContext?.active
|
|
16611
|
+
? null
|
|
16612
|
+
: projectedUserTaskBinding,
|
|
16499
16613
|
},
|
|
16500
16614
|
);
|
|
16501
16615
|
if (canonicalProviderContext.plan) {
|
|
@@ -16761,6 +16875,78 @@ export async function* agentLoop(messages, options) {
|
|
|
16761
16875
|
throw error;
|
|
16762
16876
|
}
|
|
16763
16877
|
|
|
16878
|
+
if (result?.finishReason === "content_filter") {
|
|
16879
|
+
// Usage has settled above. A provider-filtered round is never a complete
|
|
16880
|
+
// answer or an executable tool batch, and must not trigger a fresh call.
|
|
16881
|
+
await _awaitBackgroundUsageSettlement(
|
|
16882
|
+
backgroundSubAgents,
|
|
16883
|
+
backgroundUsageFailureState,
|
|
16884
|
+
);
|
|
16885
|
+
const error = new Error(
|
|
16886
|
+
"The provider filtered this response. No tools from that response were executed; the task remains incomplete.",
|
|
16887
|
+
);
|
|
16888
|
+
error.code = "CC_MODEL_CONTENT_FILTER";
|
|
16889
|
+
throw error;
|
|
16890
|
+
}
|
|
16891
|
+
|
|
16892
|
+
if (
|
|
16893
|
+
result?.providerTerminationComplete === false &&
|
|
16894
|
+
result.finishReason != null &&
|
|
16895
|
+
result.finishReason !== "length"
|
|
16896
|
+
) {
|
|
16897
|
+
// Explicit ambiguity is not an executable or final provider round.
|
|
16898
|
+
// Keep absent-reason compatibility and the separate length policy.
|
|
16899
|
+
await _awaitBackgroundUsageSettlement(
|
|
16900
|
+
backgroundSubAgents,
|
|
16901
|
+
backgroundUsageFailureState,
|
|
16902
|
+
);
|
|
16903
|
+
const error = new Error(
|
|
16904
|
+
"The provider response has an incomplete or ambiguous termination. No tools from that response were executed; the task remains incomplete.",
|
|
16905
|
+
);
|
|
16906
|
+
error.code = "CC_MODEL_TERMINATION_INCOMPLETE";
|
|
16907
|
+
throw error;
|
|
16908
|
+
}
|
|
16909
|
+
|
|
16910
|
+
if (result?.finishReason === "length") {
|
|
16911
|
+
// Settle the completed physical request above before any decision. Never
|
|
16912
|
+
// execute even syntactically valid calls from an incomplete generation,
|
|
16913
|
+
// and never insert its partial assistant/tool group into canonical history.
|
|
16914
|
+
const canRecover =
|
|
16915
|
+
result.providerTerminationComplete === true &&
|
|
16916
|
+
!msg?._truncated &&
|
|
16917
|
+
_hasCompleteProviderUsage(result.usage) &&
|
|
16918
|
+
!outputLimitRecoveryUsed &&
|
|
16919
|
+
budget.hasRemaining() &&
|
|
16920
|
+
!prInvestigationExhausted &&
|
|
16921
|
+
!inspectionSynthesisRequired;
|
|
16922
|
+
if (!canRecover) {
|
|
16923
|
+
await _awaitBackgroundUsageSettlement(
|
|
16924
|
+
backgroundSubAgents,
|
|
16925
|
+
backgroundUsageFailureState,
|
|
16926
|
+
);
|
|
16927
|
+
const error = new Error(
|
|
16928
|
+
"Model output reached its token limit. No tools from that response were executed; the task remains incomplete.",
|
|
16929
|
+
);
|
|
16930
|
+
error.code = "CC_MODEL_OUTPUT_LIMIT";
|
|
16931
|
+
throw error;
|
|
16932
|
+
}
|
|
16933
|
+
outputLimitRecoveryUsed = true;
|
|
16934
|
+
outputLimitRecoveryPending =
|
|
16935
|
+
"The previous model response reached its output token limit and was discarded. " +
|
|
16936
|
+
"None of its tool calls were executed. Continue from the last confirmed tool results. " +
|
|
16937
|
+
"Produce one small, complete operation per response. For large files, write a minimal " +
|
|
16938
|
+
"complete scaffold first, then use separate small edit_file calls to add sections. " +
|
|
16939
|
+
"Do not resend the entire large file or continue an unfinished JSON fragment. " +
|
|
16940
|
+
"If no tools are needed, give a shorter complete answer.";
|
|
16941
|
+
yield {
|
|
16942
|
+
type: "iteration-warning",
|
|
16943
|
+
message:
|
|
16944
|
+
"Model output was truncated; requesting one smaller complete step within the remaining budget.",
|
|
16945
|
+
};
|
|
16946
|
+
// This is a fresh budgeted iteration with new input, never a transport retry.
|
|
16947
|
+
continue;
|
|
16948
|
+
}
|
|
16949
|
+
|
|
16764
16950
|
if (!msg) {
|
|
16765
16951
|
await _awaitBackgroundUsageSettlement(
|
|
16766
16952
|
backgroundSubAgents,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
1
2
|
import { canonicalJson } from "@chainlesschain/context-memory-kernel";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -119,6 +120,8 @@ export function mergeConsecutiveMessages(messages) {
|
|
|
119
120
|
* or folding newly added interrupt/feedback turns. Canonical fingerprints also
|
|
120
121
|
* recognize the same retained messages after compaction recreates objects.
|
|
121
122
|
*/
|
|
123
|
+
const RESUME_ROLE_PROJECTIONS = new WeakMap();
|
|
124
|
+
|
|
122
125
|
export function createResumeRoleProjection(messages) {
|
|
123
126
|
const pairs = new Map();
|
|
124
127
|
const pairKey = (left, right) => canonicalJson([left, right]);
|
|
@@ -137,20 +140,81 @@ export function createResumeRoleProjection(messages) {
|
|
|
137
140
|
pairs.set(key, (pairs.get(key) || 0) + 1);
|
|
138
141
|
}
|
|
139
142
|
}
|
|
140
|
-
|
|
143
|
+
const project = (requestMessages) => {
|
|
141
144
|
const remaining = new Map(pairs);
|
|
142
145
|
const projected = [];
|
|
146
|
+
const sources = [];
|
|
143
147
|
for (let index = 0; index < requestMessages.length; index++) {
|
|
144
148
|
const message = requestMessages[index];
|
|
145
149
|
const key =
|
|
146
150
|
index > 0 ? pairKey(requestMessages[index - 1], message) : null;
|
|
147
151
|
if (key && remaining.get(key) > 0) {
|
|
148
152
|
const previous = projected.pop();
|
|
153
|
+
const previousSources = sources.pop();
|
|
149
154
|
projected.push(...mergeConsecutiveMessages([previous, message]));
|
|
155
|
+
sources.push([...previousSources, index]);
|
|
150
156
|
remaining.set(key, remaining.get(key) - 1);
|
|
151
|
-
} else
|
|
157
|
+
} else {
|
|
158
|
+
projected.push(message);
|
|
159
|
+
sources.push([index]);
|
|
160
|
+
}
|
|
152
161
|
}
|
|
153
|
-
return projected;
|
|
162
|
+
return { messages: projected, sources };
|
|
163
|
+
};
|
|
164
|
+
const projection = (requestMessages) => project(requestMessages).messages;
|
|
165
|
+
RESUME_ROLE_PROJECTIONS.set(projection, project);
|
|
166
|
+
return projection;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Preserve a bound task through this module's exact, captured resume merges.
|
|
171
|
+
* Arbitrary callbacks, wrappers and forged properties never receive lineage.
|
|
172
|
+
*/
|
|
173
|
+
export function projectMessagesWithTaskBinding(projection, input, taskBinding) {
|
|
174
|
+
const owned = RESUME_ROLE_PROJECTIONS.get(projection);
|
|
175
|
+
if (!owned) {
|
|
176
|
+
return { messages: projection(input), taskBinding };
|
|
177
|
+
}
|
|
178
|
+
const projected = owned(input);
|
|
179
|
+
if (!taskBinding) return { messages: projected.messages, taskBinding };
|
|
180
|
+
const identities = input.flatMap((message, index) =>
|
|
181
|
+
message === taskBinding.original ? [index] : [],
|
|
182
|
+
);
|
|
183
|
+
const candidates = identities.length
|
|
184
|
+
? identities
|
|
185
|
+
: taskBinding.allowContentRelocation === true
|
|
186
|
+
? input.map((_, index) => index)
|
|
187
|
+
: [];
|
|
188
|
+
const matches = candidates.filter(
|
|
189
|
+
(index) =>
|
|
190
|
+
input[index]?.role === "user" &&
|
|
191
|
+
isDeepStrictEqual(input[index], taskBinding.message),
|
|
192
|
+
);
|
|
193
|
+
// Leave invalid bindings intact so canonical admission rejects them normally.
|
|
194
|
+
if (matches.length !== 1 || identities.length > 1)
|
|
195
|
+
return { messages: projected.messages, taskBinding };
|
|
196
|
+
const sourceIndex = matches[0];
|
|
197
|
+
const targetIndex = projected.sources.findIndex((group) =>
|
|
198
|
+
group.includes(sourceIndex),
|
|
199
|
+
);
|
|
200
|
+
const target = projected.messages[targetIndex];
|
|
201
|
+
if (target === input[sourceIndex])
|
|
202
|
+
return { messages: projected.messages, taskBinding };
|
|
203
|
+
// A merge may discard right-message metadata. Only bare user content has a
|
|
204
|
+
// complete preservation proof under the established joinContent contract.
|
|
205
|
+
if (
|
|
206
|
+
Object.keys(taskBinding.message).some(
|
|
207
|
+
(key) => !["role", "content"].includes(key),
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
return { messages: projected.messages, taskBinding };
|
|
211
|
+
return {
|
|
212
|
+
messages: projected.messages,
|
|
213
|
+
taskBinding: {
|
|
214
|
+
message: JSON.parse(canonicalJson(target)),
|
|
215
|
+
original: target,
|
|
216
|
+
allowContentRelocation: false,
|
|
217
|
+
},
|
|
154
218
|
};
|
|
155
219
|
}
|
|
156
220
|
|
|
@@ -291,6 +291,9 @@ export function _openaiCachedTokens(usage) {
|
|
|
291
291
|
export function _openaiInitState() {
|
|
292
292
|
return {
|
|
293
293
|
responseId: null,
|
|
294
|
+
finishReason: null,
|
|
295
|
+
streamDone: false,
|
|
296
|
+
terminationInvalid: false,
|
|
294
297
|
text: "",
|
|
295
298
|
tools: [],
|
|
296
299
|
inputTokens: null,
|
|
@@ -304,13 +307,39 @@ export function _openaiReduceLine(state, raw, onToken) {
|
|
|
304
307
|
const line = (raw || "").trim();
|
|
305
308
|
if (!line.startsWith("data:")) return state;
|
|
306
309
|
const payload = line.slice(5).trim();
|
|
307
|
-
if (!payload
|
|
310
|
+
if (!payload) return state;
|
|
311
|
+
if (payload === "[DONE]") {
|
|
312
|
+
if (!state.finishReason || state.streamDone)
|
|
313
|
+
state.terminationInvalid = true;
|
|
314
|
+
state.streamDone = true;
|
|
315
|
+
return state;
|
|
316
|
+
}
|
|
308
317
|
let obj;
|
|
309
318
|
try {
|
|
310
319
|
obj = JSON.parse(payload);
|
|
311
320
|
} catch {
|
|
321
|
+
state.terminationInvalid = true;
|
|
312
322
|
return state;
|
|
313
323
|
}
|
|
324
|
+
if (state.streamDone || obj.error) state.terminationInvalid = true;
|
|
325
|
+
if (!Array.isArray(obj.choices) || obj.choices.length > 1) {
|
|
326
|
+
state.terminationInvalid = true;
|
|
327
|
+
}
|
|
328
|
+
const choice = obj.choices?.[0];
|
|
329
|
+
if (choice) {
|
|
330
|
+
if (state.finishReason || (choice.index != null && choice.index !== 0))
|
|
331
|
+
state.terminationInvalid = true;
|
|
332
|
+
if (choice.finish_reason != null) {
|
|
333
|
+
// Terminal filtering cannot be downgraded to stop/tools by a later
|
|
334
|
+
// conflicting frame. Length likewise remains non-complete.
|
|
335
|
+
if (
|
|
336
|
+
state.finishReason !== "content_filter" &&
|
|
337
|
+
(state.finishReason !== "length" ||
|
|
338
|
+
choice.finish_reason === "content_filter")
|
|
339
|
+
)
|
|
340
|
+
state.finishReason = choice.finish_reason;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
314
343
|
if (!state.responseId) state.responseId = _providerReceiptId(obj.id);
|
|
315
344
|
const pricing = projectEnvelopePricingContext(obj.usage || {}, obj);
|
|
316
345
|
for (const [key, value] of Object.entries(pricing)) {
|
|
@@ -376,6 +405,11 @@ export function _openaiFinalize(state) {
|
|
|
376
405
|
const message = { role: "assistant", content: state.text };
|
|
377
406
|
if (toolCalls.length) message.tool_calls = toolCalls;
|
|
378
407
|
const out = { message };
|
|
408
|
+
if (state.finishReason != null) {
|
|
409
|
+
out.finishReason = state.finishReason;
|
|
410
|
+
out.providerTerminationComplete =
|
|
411
|
+
state.streamDone && !state.terminationInvalid;
|
|
412
|
+
}
|
|
379
413
|
if (
|
|
380
414
|
state.usageInvalid !== true &&
|
|
381
415
|
_isNonNegativeSafeInteger(state.inputTokens) &&
|