narraeon 0.4.0 → 0.5.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 (76) hide show
  1. package/dist/node/protocol/appPreferences.js +25 -0
  2. package/dist/node/protocol/conversationObservation.js +67 -0
  3. package/dist/node/protocol/v1.js +186 -34
  4. package/dist/node/protocol/worldExtensions.js +16 -0
  5. package/dist/node/protocol/worldMaintenance.js +77 -0
  6. package/dist/node/protocol/worldPromptDiagnostics.js +1 -0
  7. package/dist/node/runtime/ConversationChanges.js +24 -0
  8. package/dist/node/runtime/V1Runtime.js +445 -121
  9. package/dist/node/runtime/artifact/FileNativeArtifactStore.js +326 -12
  10. package/dist/node/runtime/authoring/AuthoringRequestSnapshot.js +63 -0
  11. package/dist/node/runtime/authoring/ModelToolConversation.js +218 -0
  12. package/dist/node/runtime/config/AppPreferencesStore.js +46 -17
  13. package/dist/node/runtime/content/ContentTreeFingerprint.js +13 -0
  14. package/dist/node/runtime/content/ContentWorkspace.js +43 -13
  15. package/dist/node/runtime/content/CurrentTreeContentLibrary.js +128 -46
  16. package/dist/node/runtime/content/FileNativeContentTree.js +28 -10
  17. package/dist/node/runtime/content/PackageFollowups.js +80 -0
  18. package/dist/node/runtime/extension/FrontendExtensionBundle.js +19 -14
  19. package/dist/node/runtime/extension/PackageScriptPermissions.js +70 -0
  20. package/dist/node/runtime/extension/WorldExtensionControls.js +205 -0
  21. package/dist/node/runtime/extension/WorldExtensionRequests.js +64 -0
  22. package/dist/node/runtime/model/FileNativeModelAdapters.js +21 -466
  23. package/dist/node/runtime/model/ModelConnectionStore.js +13 -0
  24. package/dist/node/runtime/model/ModelHost.js +31 -1
  25. package/dist/node/runtime/model/ModelHostStream.js +51 -5
  26. package/dist/node/runtime/play/FileNativeContinuityCorrection.js +55 -45
  27. package/dist/node/runtime/play/FileNativePlayPresetStore.js +256 -238
  28. package/dist/node/runtime/play/FileNativePlayTimelineStore.js +118 -9
  29. package/dist/node/runtime/play/OrderedFollowups.js +71 -0
  30. package/dist/node/runtime/play/OrderedPlayPrompts.js +180 -0
  31. package/dist/node/runtime/play/PlayCallChain.js +814 -61
  32. package/dist/node/runtime/play/PlayContinuity.js +88 -0
  33. package/dist/node/runtime/play/PlayDocumentTools.js +205 -287
  34. package/dist/node/runtime/play/PlayFollowupRequests.js +34 -3
  35. package/dist/node/runtime/play/PlayPresetWorkbench.js +45 -54
  36. package/dist/node/runtime/play/PlayPromptRun.js +86 -0
  37. package/dist/node/runtime/prompt/FileNativePromptCompiler.js +670 -93
  38. package/dist/node/runtime/prompt/FileNativeToolRegistry.js +37 -21
  39. package/dist/node/runtime/prompt/PromptCompilationCodec.js +322 -0
  40. package/dist/node/runtime/prompt/WorldMaintenanceReport.js +251 -0
  41. package/dist/node/runtime/prompt/WorldMaterialCoverage.js +83 -0
  42. package/dist/node/runtime/prompt/WorldPromptDiagnostics.js +185 -0
  43. package/dist/node/runtime/setting/FileNativeSettingImprovementStore.js +1027 -0
  44. package/dist/node/runtime/setting/SettingAuthoringTransaction.js +1339 -0
  45. package/dist/node/runtime/setting/SettingImprovementSession.js +714 -0
  46. package/dist/node/runtime/world/FileNativeAuthorityV3.js +88 -1
  47. package/dist/node/runtime/world/FileNativeWorldStore.js +292 -27
  48. package/dist/node/runtime/world/PlayerViewBindings.js +30 -0
  49. package/dist/node/runtime/world/PlayerViewRenderer.js +27 -34
  50. package/dist/node/runtime/world/WorldDocumentMaintenance.js +174 -0
  51. package/dist/node/runtime/world/WorldDocumentStore.js +166 -96
  52. package/dist/node/runtime/world/WorldOperationCoordinator.js +103 -2
  53. package/dist/node/runtime/world-revision/FileNativeWorldRevisionStore.js +696 -0
  54. package/dist/node/runtime/world-revision/WorldRevisionAuthoringTransaction.js +380 -0
  55. package/dist/node/runtime/world-revision/WorldRevisionSession.js +709 -0
  56. package/dist/node/runtime/world-revision/WorldRevisionWorkspace.js +494 -0
  57. package/dist/node/server/ConversationStream.js +109 -0
  58. package/dist/node/server/createServer.js +5 -1
  59. package/dist/node/shared/artifact-payload-contract.js +123 -0
  60. package/dist/node/shared/default-play-prompts.js +25 -45
  61. package/dist/node/shared/default-preset-host.js +114 -138
  62. package/dist/node/shared/default-setting-improvement-prompt.js +52 -38
  63. package/dist/node/shared/display-regex.js +114 -0
  64. package/dist/node/shared/first-party-action-choices.js +1 -6
  65. package/dist/node/shared/first-party-player-view.js +1 -6
  66. package/dist/node/shared/ordered-author-prompts.js +64 -0
  67. package/dist/node/shared/ordered-followups.js +42 -0
  68. package/dist/node/shared/ordered-play-prompts.js +73 -0
  69. package/dist/web/assets/index-BXfelCi9.js +1207 -0
  70. package/dist/web/assets/index-BYnhUxlk.css +1 -0
  71. package/dist/web/index.html +2 -2
  72. package/package.json +1 -1
  73. package/dist/node/runtime/model/SettingAuthorStream.js +0 -21
  74. package/dist/node/runtime/setting/DocumentCandidateSettingImprovement.js +0 -1731
  75. package/dist/web/assets/index-D8YKfmNG.js +0 -1192
  76. package/dist/web/assets/index-Xa0s35CJ.css +0 -1
@@ -1,11 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { appendFile, chmod, mkdir, stat } from "node:fs/promises";
3
3
  import { dirname } from "node:path";
4
- import { defaultAppLocale, } from "../../protocol/appPreferences.js";
5
4
  import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, } from "../../protocol/modelConnections.js";
6
5
  import { ModelHostBindingMismatchError, ModelHostCancelledError, ModelHostContinuationError, ModelHostFailureError, ModelHostOutcomeUnknownError, } from "./ModelHost.js";
7
6
  import { renderPromptDeltaMessage, } from "../prompt/FileNativePromptCompiler.js";
8
- import { aggregateAnthropicSettingStream, aggregateChatSettingStream, } from "./SettingAuthorStream.js";
9
7
  import { aggregateAnthropicModelStream, aggregateChatModelStream, aggregateResponsesModelStream, } from "./ModelHostStream.js";
10
8
  import { defaultRuntimeToolDefinitionStrategy, portableRuntimeToolInputSchema, } from "../prompt/FileNativeToolRegistry.js";
11
9
  import { AiExchangeCapture, errorDescription, } from "./AiFailureLog.js";
@@ -347,9 +345,11 @@ function modelDiagnosticContext(request) {
347
345
  return {
348
346
  scope: request.requestId === "play_call_chain"
349
347
  ? "play_call_chain"
350
- : request.operationId?.includes(":followup:") === true
351
- ? "play_followup"
352
- : "model_host",
348
+ : request.operationId?.startsWith("setting-") === true
349
+ ? "setting_improvement"
350
+ : request.operationId?.includes(":followup:") === true
351
+ ? "play_followup"
352
+ : "model_host",
353
353
  ...(request.requestId === undefined
354
354
  ? {}
355
355
  : { requestId: request.requestId }),
@@ -376,15 +376,6 @@ function captureDelta(capture, downstream) {
376
376
  downstream?.(delta);
377
377
  };
378
378
  }
379
- function captureSettingDelta(capture, downstream) {
380
- if (capture === undefined)
381
- return downstream;
382
- return (delta) => {
383
- if (delta.kind === "reasoning")
384
- capture.observeReasoning(delta.text);
385
- downstream?.(delta);
386
- };
387
- }
388
379
  async function capturedProviderOperation(failureLog, capture, operation) {
389
380
  const observeResponse = (response) => capture?.captureResponse(response) ?? response;
390
381
  try {
@@ -407,6 +398,10 @@ async function capturedProviderOperation(failureLog, capture, operation) {
407
398
  message: error instanceof Error
408
399
  ? error.message
409
400
  : "Provider request processing failed.",
401
+ ...(error instanceof ModelHostFailureError &&
402
+ error.details !== undefined
403
+ ? { details: error.details }
404
+ : {}),
410
405
  error: errorDescription(error),
411
406
  };
412
407
  await failureLog.recordFailure({ exchange, failures: [failure] });
@@ -432,323 +427,6 @@ async function trace(kind, value) {
432
427
  // Explicit acceptance diagnostics must never change provider semantics.
433
428
  }
434
429
  }
435
- export class FileNativeSettingAuthorProvider {
436
- #connection;
437
- #fetch;
438
- #failureLog;
439
- #operationId;
440
- #locale;
441
- #exchange = 0;
442
- constructor(connection, fetchImplementation = fetch, diagnostics) {
443
- this.#connection = resolveConnection(connection);
444
- this.#fetch = fetchImplementation;
445
- this.#failureLog = diagnostics?.failureLog;
446
- this.#operationId = diagnostics?.operationId;
447
- this.#locale = diagnostics?.locale ?? defaultAppLocale;
448
- }
449
- async next(request) {
450
- this.#exchange += 1;
451
- const diagnosticContext = {
452
- scope: "setting_improvement",
453
- requestId: "setting_improvement",
454
- ...(this.#operationId === undefined
455
- ? {}
456
- : { operationId: this.#operationId }),
457
- requestAttempt: 1,
458
- exchange: this.#exchange,
459
- };
460
- const tools = request.tools.map((name) => settingToolDefinition(name, this.#locale));
461
- if (this.#connection.provider === "chat_completions") {
462
- const body = {
463
- model: this.#connection.modelId,
464
- ...chatReasoningRequest(this.#connection),
465
- messages: chatSettingMessages(this.#connection, request.messages),
466
- ...(this.#connection.dialect === "cliproxyapi"
467
- ? {
468
- prompt_cache_key: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
469
- }
470
- : {}),
471
- ...(tools.length === 0
472
- ? {}
473
- : {
474
- tools: tools.map((tool) => ({
475
- type: "function",
476
- function: {
477
- name: tool.name,
478
- description: tool.description,
479
- parameters: tool.schema,
480
- },
481
- })),
482
- }),
483
- max_tokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
484
- stream: true,
485
- stream_options: { include_usage: true },
486
- };
487
- await trace("setting_chat_request", body);
488
- const bodyJson = JSON.stringify(body);
489
- const url = providerUrl(this.#connection);
490
- const capture = providerCapture(this.#failureLog, this.#connection.provider, url, diagnosticContext, bodyJson);
491
- const diagnosticDelta = captureSettingDelta(capture, request.onDelta);
492
- return capturedProviderOperation(this.#failureLog, capture, async (observe) => {
493
- const response = observe(await this.#fetch(url, {
494
- method: "POST",
495
- headers: {
496
- Authorization: `Bearer ${this.#connection.apiKey}`,
497
- "Content-Type": "application/json",
498
- },
499
- body: bodyJson,
500
- }));
501
- if (!response.ok)
502
- throw await providerError(response);
503
- if (response.body === null)
504
- throw new Error("The authoring response is missing a streaming body");
505
- const streamed = await aggregateChatSettingStream(response.body, diagnosticDelta);
506
- capture?.setReasoning(streamed.reasoningContent);
507
- await trace("setting_chat_response", streamed);
508
- return {
509
- role: "assistant",
510
- content: streamed.content,
511
- ...(streamed.reasoningContent === ""
512
- ? {}
513
- : { reasoningContent: streamed.reasoningContent }),
514
- providerState: {
515
- protocol: "chat_completions",
516
- assistantMessage: cloneProviderValue(streamed.assistantMessage),
517
- },
518
- toolCalls: streamed.toolCalls.map((call) => ({
519
- id: call.id,
520
- name: call.name,
521
- arguments: asArguments(parseArguments(call.arguments)),
522
- })),
523
- usage: settingAuthorUsage(normalizeChatUsage(streamed.usage)),
524
- };
525
- });
526
- }
527
- if (this.#connection.provider === "openai_responses") {
528
- const response = await openAIResponsesRequest({
529
- connection: this.#connection,
530
- fetchImplementation: this.#fetch,
531
- input: responsesSettingInput(this.#connection, request.messages),
532
- tools: tools.map((tool) => ({
533
- name: tool.name,
534
- description: tool.description,
535
- parameters: tool.schema,
536
- })),
537
- maxOutputTokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
538
- promptCacheKey: settingPromptCacheKey(this.#connection, request.messages, this.#operationId),
539
- tracePrefix: "setting_responses",
540
- ...(this.#failureLog === undefined
541
- ? {}
542
- : { failureLog: this.#failureLog }),
543
- diagnosticContext,
544
- ...(request.onDelta === undefined ? {} : { onDelta: request.onDelta }),
545
- });
546
- return {
547
- role: "assistant",
548
- content: response.text ?? "",
549
- ...(response.reasoningContent === undefined
550
- ? {}
551
- : { reasoningContent: response.reasoningContent }),
552
- providerState: response.providerState,
553
- toolCalls: (response.toolCalls ?? []).map((call) => ({
554
- ...call,
555
- arguments: asArguments(call.arguments),
556
- })),
557
- usage: settingAuthorUsage(response.usage),
558
- ...(response.diagnostics === undefined
559
- ? {}
560
- : { diagnostics: response.diagnostics }),
561
- };
562
- }
563
- const system = request.messages
564
- .filter(({ role }) => role === "system")
565
- .map(({ content }, index, messages) => ({
566
- type: "text",
567
- text: content,
568
- ...(index === messages.length - 1
569
- ? { cache_control: { type: "ephemeral" } }
570
- : {}),
571
- }));
572
- const body = {
573
- model: this.#connection.modelId,
574
- ...anthropicReasoningRequest(this.#connection),
575
- ...(system.length === 0 ? {} : { system }),
576
- messages: anthropicSettingMessages(request.messages),
577
- ...(tools.length === 0
578
- ? {}
579
- : {
580
- tools: tools.map((tool) => ({
581
- name: tool.name,
582
- description: tool.description,
583
- input_schema: tool.schema,
584
- })),
585
- }),
586
- max_tokens: Math.min(request.maxOutputTokens, this.#connection.maxOutputTokens),
587
- stream: true,
588
- };
589
- await trace("setting_anthropic_request", body);
590
- const bodyJson = JSON.stringify(body);
591
- const url = providerUrl(this.#connection);
592
- const capture = providerCapture(this.#failureLog, this.#connection.provider, url, diagnosticContext, bodyJson);
593
- const diagnosticDelta = captureSettingDelta(capture, request.onDelta);
594
- return capturedProviderOperation(this.#failureLog, capture, async (observe) => {
595
- const response = observe(await this.#fetch(url, {
596
- method: "POST",
597
- headers: {
598
- "anthropic-version": "2023-06-01",
599
- "x-api-key": this.#connection.apiKey,
600
- "Content-Type": "application/json",
601
- },
602
- body: bodyJson,
603
- }));
604
- if (!response.ok)
605
- throw await providerError(response);
606
- if (response.body === null)
607
- throw new Error("The authoring response is missing a streaming body");
608
- const streamed = await aggregateAnthropicSettingStream(response.body, diagnosticDelta);
609
- capture?.setReasoning(streamed.reasoningContent);
610
- await trace("setting_anthropic_response", streamed);
611
- return {
612
- role: "assistant",
613
- content: streamed.content,
614
- ...(streamed.reasoningContent === ""
615
- ? {}
616
- : { reasoningContent: streamed.reasoningContent }),
617
- providerState: {
618
- protocol: "anthropic_messages",
619
- content: cloneProviderValue(streamed.providerContent),
620
- ...(streamed.responseId === undefined
621
- ? {}
622
- : { responseId: streamed.responseId }),
623
- ...(streamed.model === undefined ? {} : { model: streamed.model }),
624
- ...(streamed.stopReason === undefined
625
- ? {}
626
- : { stopReason: streamed.stopReason }),
627
- },
628
- toolCalls: streamed.toolCalls.map((call) => ({
629
- id: call.id,
630
- name: call.name,
631
- arguments: asArguments(structuredClone(call.arguments)),
632
- })),
633
- usage: settingAuthorUsage(normalizeAnthropicUsage(streamed.usage)),
634
- };
635
- });
636
- }
637
- }
638
- // Setting authoring uses the same complete usage projection as play. Missing
639
- // Provider usage stays unavailable instead of being counted as zero.
640
- function settingAuthorUsage(usage) {
641
- if (usage !== undefined)
642
- return structuredClone(usage);
643
- return usageRecord({
644
- inputTokens: null,
645
- uncachedInputTokens: null,
646
- cacheReadTokens: null,
647
- cacheWriteTokens: null,
648
- reasoningTokens: null,
649
- outputTokens: null,
650
- totalTokens: null,
651
- });
652
- }
653
- function settingToolDefinition(name, locale = defaultAppLocale) {
654
- const descriptions = settingToolDescriptions[locale];
655
- const schema = (properties, required) => ({
656
- type: "object",
657
- additionalProperties: false,
658
- properties,
659
- required,
660
- });
661
- const text = { type: "string", minLength: 1 };
662
- const path = { type: "string", minLength: 1 };
663
- const cursor = { type: "string", minLength: 1 };
664
- const limit = { type: "integer", minimum: 1, maximum: 100 };
665
- const definitions = {
666
- setting_list: {
667
- description: descriptions.setting_list,
668
- schema: schema({
669
- // World document paths accept any segment that is not "."/".."; an
670
- // ASCII-only pattern here made directories the author had just
671
- // created impossible to list back.
672
- directory: {
673
- type: "string",
674
- pattern: "^world(?:/[^\\\\]+)?$",
675
- },
676
- limit,
677
- cursor,
678
- }, []),
679
- },
680
- setting_search: {
681
- description: descriptions.setting_search,
682
- schema: schema({
683
- query: { type: "string", minLength: 1, maxLength: 256 },
684
- within: path,
685
- caseSensitive: { type: "boolean" },
686
- limit,
687
- cursor,
688
- }, ["query"]),
689
- },
690
- setting_read: {
691
- description: descriptions.setting_read,
692
- schema: schema({
693
- path,
694
- maxBytes: { type: "integer", minimum: 4, maximum: 65_536 },
695
- cursor,
696
- }, ["path"]),
697
- },
698
- setting_write_file: {
699
- description: descriptions.setting_write_file,
700
- schema: schema({ path, contents: text }, ["path", "contents"]),
701
- },
702
- setting_patch: {
703
- description: descriptions.setting_patch,
704
- schema: schema({
705
- document: text,
706
- op: { type: "string", enum: ["add", "replace"] },
707
- locator: {
708
- type: "array",
709
- minItems: 1,
710
- items: { type: "string", minLength: 1 },
711
- },
712
- value: {},
713
- }, ["document", "op", "locator", "value"]),
714
- },
715
- setting_move: {
716
- description: descriptions.setting_move,
717
- schema: schema({ from: text, to: path }, ["from", "to"]),
718
- },
719
- setting_preview_candidate: {
720
- description: descriptions.setting_preview_candidate,
721
- schema: schema({}, []),
722
- },
723
- setting_finish_candidate: {
724
- description: descriptions.setting_finish_candidate,
725
- schema: schema({}, []),
726
- },
727
- };
728
- return { name, ...definitions[name] };
729
- }
730
- const settingToolDescriptions = {
731
- en: {
732
- setting_list: "List the world root or a world/ subdirectory from the current candidate-document snapshot. The root also lists specialized opening and control files. A pagination cursor is valid only for the same query against the candidate snapshot that produced it.",
733
- setting_search: "Search literal source text in world documents from the current candidate snapshot. within may be a world/ directory, world/ document path, @short-ref, or document identity.",
734
- setting_read: "Precisely read a world-document body from the current candidate snapshot by world/ path, @short-ref, or document identity. Read opening and control files through their exact dedicated paths. Write authorization is granted only after every page has been read; later candidate changes do not revoke it.",
735
- setting_write_file: "Create or replace one complete candidate file. path is always a full logical path: a .yaml or .md world document under world/, control/frame.yaml, control/player-views.yaml, control/blocks/*.md, or the root opening.md. A new world document must include a complete body with its $document technical header. When replacing an existing world document, you may omit $document and write back the body returned by setting_read; Runtime preserves the existing title, summary, and aliases. Runtime always decides id and ref: it allocates them for a new document and preserves them for an existing one, regardless of values you supply. Existing world documents and an existing opening.md must be read completely before replacement.",
736
- setting_patch: 'Patch one YAML map node by document id, @short-ref, or world/ logical path and revision. op must explicitly be add or replace. locator is a stable array of map keys, for example ["relationships","player","trust"]. The existing document must already have been read completely. Rewrite Markdown documents in full with setting_write_file.',
737
- setting_move: "Move a completely read world document to a new world/ logical path by revision. Use this to repair catalog-directory association while preserving document content and identity.",
738
- setting_preview_candidate: "Run mechanical content-tree checks and the real Prompt Preview against the isolated candidate, returning diagnostics for path association, slots, references, and material coverage. Call this after all final changes; the candidate may finish only after it passes. A failed preview is a normal iteration: repair the diagnostics and call it again.",
739
- setting_finish_candidate: "Call only after setting_preview_candidate has passed for the final changes. It may immediately follow the passing preview in the same model response. It must be the last call in that response and takes no arguments.",
740
- },
741
- "zh-CN": {
742
- setting_list: "通过当前候选文档快照列出 world 或 world/ 子目录;根目录同时显示 opening/control 等专用文件。分页 cursor 只能用于产生它的同一候选快照和相同查询。",
743
- setting_search: "通过当前候选文档快照原文字面搜索 world 文档;within 可取 world/ 目录、world/ 文档路径、@短引用或文档身份。",
744
- setting_read: "通过当前候选文档快照按 world/ 路径、@短引用或文档身份精确读取世界文档正文;opening/control 使用精确路径专用读取。只有读完全部分页才获得写授权,此后不因其他修改失效。",
745
- setting_write_file: "创建或整份写入一个候选文件,path 一律是完整逻辑路径:world/ 下的 .yaml 或 .md 世界文档、control/frame.yaml、control/player-views.yaml、control/blocks/*.md,或根级 opening.md。新建世界文档写完整原文并带 $document 技术头;覆盖既有世界文档可以省略 $document,直接写回 setting_read 返回的正文,Runtime 会沿用原有 title、summary 和 aliases。id 和 ref 始终由 Runtime 决定——新建时分配,覆盖时保留原值,你写什么都不会报错。既有 world 文档和既有 opening.md 必须先完整读取。",
746
- setting_patch: '通过 revision 按文档 ID、@短引用或 world/ 逻辑路径 patch 一个 YAML map 节点;op 必须明确选择 add(新增)或 replace(替换),locator 是稳定 map-key 数组,例如 ["关系","对玩家","信任"]。既有文档必须已完整读取。Markdown 文档请用 setting_write_file 整份重写。',
747
- setting_move: "通过 revision 把已完整读取的 world 文档移动到新的 world/ 逻辑路径;用于修复 catalog 目录关联,保留文档内容和身份。",
748
- setting_preview_candidate: "对当前隔离候选运行内容树机械检查和真实 Prompt Preview,并返回路径关联、slot、引用和材料覆盖诊断;所有最终修改后调用,通过后才可结束候选。未通过属于正常迭代,修复后重新调用即可。",
749
- setting_finish_candidate: "setting_preview_candidate 已对最终修改返回自检通过后调用,可以紧接在同一模型响应的自检之后;必须是当前响应的最后一个调用,且不得携带参数。",
750
- },
751
- };
752
430
  function items(request) {
753
431
  return request.appended;
754
432
  }
@@ -860,6 +538,9 @@ function responsesRequestBody(input) {
860
538
  name: tool.name,
861
539
  description: tool.description,
862
540
  parameters: tool.parameters,
541
+ // Runtime schemas distinguish absent options from supplied values.
542
+ // Responses otherwise normalizes compatible schemas to strict mode.
543
+ strict: false,
863
544
  })),
864
545
  tool_choice: toolChoice,
865
546
  }),
@@ -967,20 +648,11 @@ function promptCacheKey(connection, request) {
967
648
  .update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
968
649
  .digest("hex");
969
650
  }
970
- function settingPromptCacheKey(connection, messages, operationId) {
971
- const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
972
- const initialPrefix = messages.slice(0, firstAssistant < 0 ? messages.length : firstAssistant);
973
- const stablePrefixScope = JSON.stringify(initialPrefix);
974
- const scope = connection.dialect === "cliproxyapi"
975
- ? (operationId ?? stablePrefixScope)
976
- : stablePrefixScope;
977
- return createHash("sha256")
978
- .update(`${connection.provider}\0${connection.dialect}\0${connection.modelId}\0${scope}`)
979
- .digest("hex");
980
- }
981
651
  function chatAppend(request) {
982
652
  return items(request).flatMap((item) => {
983
- if (item.kind === "player")
653
+ if (item.kind === "player" ||
654
+ item.kind === "user" ||
655
+ item.kind === "runtime_notice")
984
656
  return [{ role: "user", content: item.text }];
985
657
  if (item.kind === "prompt_delta")
986
658
  return promptDeltaProviderMessages(item.logicalMessages);
@@ -1020,7 +692,9 @@ function anthropicAppend(request) {
1020
692
  continue;
1021
693
  }
1022
694
  flushToolResults();
1023
- if (item.kind === "player") {
695
+ if (item.kind === "player" ||
696
+ item.kind === "user" ||
697
+ item.kind === "runtime_notice") {
1024
698
  messages.push({ role: "user", content: item.text });
1025
699
  continue;
1026
700
  }
@@ -1043,7 +717,9 @@ function anthropicAppend(request) {
1043
717
  }
1044
718
  function responsesAppend(request) {
1045
719
  return items(request).flatMap((item) => {
1046
- if (item.kind === "player")
720
+ if (item.kind === "player" ||
721
+ item.kind === "user" ||
722
+ item.kind === "runtime_notice")
1047
723
  return [{ role: "user", content: item.text }];
1048
724
  if (item.kind === "prompt_delta")
1049
725
  return promptDeltaProviderMessages(item.logicalMessages);
@@ -1173,122 +849,6 @@ async function providerError(response) {
1173
849
  });
1174
850
  return new ModelHostFailureError(`Provider request failed: ${response.status} ${text}`);
1175
851
  }
1176
- function chatSettingMessages(connection, messages) {
1177
- const translated = messages.map(chatSettingMessage);
1178
- if (connection.dialect !== "cliproxyapi")
1179
- return translated;
1180
- return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
1181
- }
1182
- function chatSettingMessage(message) {
1183
- if (message.role === "tool")
1184
- return {
1185
- role: "tool",
1186
- tool_call_id: message.toolCallId,
1187
- content: message.content,
1188
- };
1189
- if (message.role === "assistant") {
1190
- const state = message.providerState;
1191
- if (state?.protocol === "chat_completions" &&
1192
- validChatAssistantMessage(state.assistantMessage))
1193
- return cloneProviderValue(state.assistantMessage);
1194
- throw continuationError("Chat Completions setting improvement");
1195
- }
1196
- return {
1197
- role: message.role === "system" ? "system" : "user",
1198
- content: message.content,
1199
- };
1200
- }
1201
- function anthropicSettingMessages(source) {
1202
- const messages = [];
1203
- const cacheableUserIndex = settingPrefixUserIndex(source);
1204
- let toolResults = [];
1205
- const flushToolResults = () => {
1206
- if (toolResults.length === 0)
1207
- return;
1208
- messages.push({ role: "user", content: toolResults });
1209
- toolResults = [];
1210
- };
1211
- for (const [index, message] of source.entries()) {
1212
- if (message.role === "system")
1213
- continue;
1214
- if (message.role === "tool") {
1215
- toolResults.push({
1216
- type: "tool_result",
1217
- tool_use_id: message.toolCallId,
1218
- content: message.content,
1219
- ...(message.isError === true ? { is_error: true } : {}),
1220
- });
1221
- continue;
1222
- }
1223
- flushToolResults();
1224
- if (message.role === "assistant") {
1225
- if (message.providerState?.protocol !== "anthropic_messages" ||
1226
- !validAnthropicContent(message.providerState.content))
1227
- throw continuationError("Anthropic Messages setting improvement");
1228
- messages.push({
1229
- role: "assistant",
1230
- content: cloneProviderValue(message.providerState.content),
1231
- });
1232
- continue;
1233
- }
1234
- messages.push({
1235
- role: "user",
1236
- content: index === cacheableUserIndex
1237
- ? [
1238
- {
1239
- type: "text",
1240
- text: message.content,
1241
- cache_control: { type: "ephemeral" },
1242
- },
1243
- ]
1244
- : message.content,
1245
- });
1246
- }
1247
- flushToolResults();
1248
- return messages;
1249
- }
1250
- function responsesSettingInput(connection, messages) {
1251
- const translated = messages.flatMap(responsesSettingMessage);
1252
- if (connection.dialect !== "cliproxyapi")
1253
- return translated;
1254
- return attachCacheControlAtIndex(attachCacheControlToLastRole(translated, "system"), settingPrefixUserIndex(messages));
1255
- }
1256
- function settingPrefixUserIndex(messages) {
1257
- const firstAssistant = messages.findIndex(({ role }) => role === "assistant");
1258
- const end = firstAssistant < 0 ? messages.length : firstAssistant;
1259
- let target = -1;
1260
- for (let index = 0; index < end; index += 1)
1261
- if (messages[index]?.role === "user")
1262
- target = index;
1263
- return target;
1264
- }
1265
- function attachCacheControlAtIndex(messages, index) {
1266
- const result = cloneProviderValue(messages);
1267
- if (index < 0 || !isRecord(result[index]))
1268
- return result;
1269
- result[index] = {
1270
- ...result[index],
1271
- cache_control: { type: "ephemeral" },
1272
- };
1273
- return result;
1274
- }
1275
- function responsesSettingMessage(message) {
1276
- if (message.role === "tool")
1277
- return [
1278
- {
1279
- type: "function_call_output",
1280
- call_id: message.toolCallId,
1281
- output: message.content,
1282
- },
1283
- ];
1284
- if (message.role === "assistant" &&
1285
- message.providerState?.protocol === "openai_responses" &&
1286
- validResponsesOutput(message.providerState.output))
1287
- return cloneResponseOutput(message.providerState.output);
1288
- if (message.role === "assistant")
1289
- throw continuationError("OpenAI Responses setting improvement");
1290
- return [{ role: message.role, content: message.content }];
1291
- }
1292
852
  function responsesBootstrapMessage(message, preserveCacheControl = false) {
1293
853
  return {
1294
854
  role: message.role,
@@ -1679,11 +1239,6 @@ function signalWasAborted(signal) {
1679
1239
  function unknownProviderResponse(label) {
1680
1240
  return new ModelHostOutcomeUnknownError(`${label} was dispatched but its response shape could not be confirmed`);
1681
1241
  }
1682
- function asArguments(value) {
1683
- if (typeof value !== "object" || value === null || Array.isArray(value))
1684
- throw new Error("Authoring tool arguments must be an object");
1685
- return value;
1686
- }
1687
1242
  function isRecord(value) {
1688
1243
  return typeof value === "object" && value !== null && !Array.isArray(value);
1689
1244
  }
@@ -2,6 +2,8 @@ import { randomUUID } from "node:crypto";
2
2
  import { chmod, mkdir, open, readFile, rename, stat, unlink, } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { hasCLIProxyThinkingSuffix, modelReasoningPolicyIssue, modelProviderPresets, } from "../../protocol/modelConnections.js";
5
+ import { FileNativeModelHost, } from "./FileNativeModelAdapters.js";
6
+ import { equalModelHostBinding, ModelHostBindingMismatchError, } from "./ModelHost.js";
5
7
  const libraryFileName = "model-connections-v1.json";
6
8
  const maxModelListBytes = 1024 * 1024;
7
9
  const maxStoredConnections = 64;
@@ -105,6 +107,17 @@ export class ModelConnectionStore {
105
107
  throw new ModelConnectionUnavailableError();
106
108
  return toBinding(connection);
107
109
  }
110
+ /** Resolve a frozen logical operation without changing the active model. */
111
+ async bindMatching(binding) {
112
+ await this.#mutationTail;
113
+ const document = await this.#readDocument();
114
+ for (const connection of document.connections) {
115
+ const candidate = toBinding(connection);
116
+ if (equalModelHostBinding(new FileNativeModelHost(candidate).binding(), binding))
117
+ return candidate;
118
+ }
119
+ throw new ModelHostBindingMismatchError("No saved model connection matches this setting-improvement conversation");
120
+ }
108
121
  async listModels(input) {
109
122
  await this.#mutationTail;
110
123
  const provider = validateProvider(input.provider);
@@ -23,14 +23,44 @@ export class ModelHostContinuationError extends Error {
23
23
  this.name = "ModelHostContinuationError";
24
24
  }
25
25
  }
26
- /** A deterministic provider failure: the operation may be failed safely. */
27
26
  export class ModelHostFailureError extends Error {
28
27
  kind = "failed";
28
+ details;
29
29
  constructor(message, options) {
30
30
  super(message, options);
31
31
  this.name = "ModelHostFailureError";
32
+ if (options?.details !== undefined)
33
+ this.details = options.details;
32
34
  }
33
35
  }
36
+ /** A Provider has rejected the complete request because its context is full. */
37
+ export function modelHostFailureRequiresFreshContext(error) {
38
+ if (!(error instanceof ModelHostFailureError))
39
+ return false;
40
+ const evidence = [error.message, ...failureDetailStrings(error.details)]
41
+ .join("\n")
42
+ .toLocaleLowerCase("und");
43
+ return [
44
+ /context[_ -](?:length|window)[_ -](?:exceeded|overflow)/u,
45
+ /maximum context length/u,
46
+ /prompt[_ -](?:is[_ -])?too[_ -]long/u,
47
+ /too many input tokens/u,
48
+ /input (?:is )?too long/u,
49
+ ].some((pattern) => pattern.test(evidence));
50
+ }
51
+ function failureDetailStrings(value, depth = 0, seen = new WeakSet()) {
52
+ if (typeof value === "string" || typeof value === "number")
53
+ return [String(value)];
54
+ if (value === null || typeof value !== "object" || depth >= 4)
55
+ return [];
56
+ if (seen.has(value))
57
+ return [];
58
+ seen.add(value);
59
+ const children = Array.isArray(value) ? value : Object.values(value);
60
+ return children
61
+ .slice(0, 64)
62
+ .flatMap((child) => failureDetailStrings(child, depth + 1, seen));
63
+ }
34
64
  /** The provider outcome cannot be replayed without risking a duplicate response. */
35
65
  export class ModelHostOutcomeUnknownError extends Error {
36
66
  kind = "unknown";