create-windy 0.8.1 → 0.8.2

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/cli.js CHANGED
@@ -33,7 +33,7 @@ var __toESM = (mod, isNodeMode, target) => {
33
33
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
34
  var __require = import.meta.require;
35
35
 
36
- // ../../node_modules/cli-width/index.js
36
+ // ../../node_modules/.bun/cli-width@4.1.0/node_modules/cli-width/index.js
37
37
  var require_cli_width = __commonJS((exports, module2) => {
38
38
  module2.exports = cliWidth;
39
39
  function normalizeOpts(options) {
@@ -73,7 +73,7 @@ var require_cli_width = __commonJS((exports, module2) => {
73
73
  }
74
74
  });
75
75
 
76
- // ../../node_modules/mute-stream/lib/index.js
76
+ // ../../node_modules/.bun/mute-stream@3.0.0/node_modules/mute-stream/lib/index.js
77
77
  var require_lib = __commonJS((exports, module2) => {
78
78
  var Stream = __require("stream");
79
79
 
@@ -4677,7 +4677,7 @@ function isMissing6(error) {
4677
4677
  import { createInterface as createInterface2 } from "readline/promises";
4678
4678
  import { stdin, stdout } from "process";
4679
4679
 
4680
- // ../../node_modules/@inquirer/core/dist/lib/key.js
4680
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/key.js
4681
4681
  var keybindings = ["emacs", "vim"];
4682
4682
  var keybindingLookup = new Set(keybindings);
4683
4683
  function isKeybinding(value) {
@@ -4694,7 +4694,7 @@ var isDownKey = (key, keybindings2 = []) => key.name === "down" || keybindings2.
4694
4694
  var isSpaceKey = (key) => key.name === "space";
4695
4695
  var isNumberKey = (key) => "1234567890".includes(key.name);
4696
4696
  var isEnterKey = (key) => key.name === "enter" || key.name === "return";
4697
- // ../../node_modules/@inquirer/core/dist/lib/errors.js
4697
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/errors.js
4698
4698
  class AbortPromptError extends Error {
4699
4699
  name = "AbortPromptError";
4700
4700
  message = "Prompt was aborted";
@@ -4720,10 +4720,10 @@ class HookError extends Error {
4720
4720
  class ValidationError extends Error {
4721
4721
  name = "ValidationError";
4722
4722
  }
4723
- // ../../node_modules/@inquirer/core/dist/lib/use-state.js
4723
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/use-state.js
4724
4724
  import { AsyncResource as AsyncResource2 } from "async_hooks";
4725
4725
 
4726
- // ../../node_modules/@inquirer/core/dist/lib/hook-engine.js
4726
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/hook-engine.js
4727
4727
  import { AsyncLocalStorage, AsyncResource } from "async_hooks";
4728
4728
  var hookStorage = new AsyncLocalStorage;
4729
4729
  function createStore(rl) {
@@ -4828,7 +4828,7 @@ var effectScheduler = {
4828
4828
  }
4829
4829
  };
4830
4830
 
4831
- // ../../node_modules/@inquirer/core/dist/lib/use-state.js
4831
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/use-state.js
4832
4832
  function isFactory(value) {
4833
4833
  return typeof value === "function";
4834
4834
  }
@@ -4849,7 +4849,7 @@ function useState(defaultValue) {
4849
4849
  });
4850
4850
  }
4851
4851
 
4852
- // ../../node_modules/@inquirer/core/dist/lib/use-effect.js
4852
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/use-effect.js
4853
4853
  function useEffect(cb, depArray) {
4854
4854
  withPointer((pointer) => {
4855
4855
  const oldDeps = pointer.get();
@@ -4861,10 +4861,10 @@ function useEffect(cb, depArray) {
4861
4861
  });
4862
4862
  }
4863
4863
 
4864
- // ../../node_modules/@inquirer/core/dist/lib/theme.js
4864
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/theme.js
4865
4865
  import { styleText } from "util";
4866
4866
 
4867
- // ../../node_modules/@inquirer/figures/dist/index.js
4867
+ // ../../node_modules/.bun/@inquirer+figures@2.0.7/node_modules/@inquirer/figures/dist/index.js
4868
4868
  import process2 from "process";
4869
4869
  function isUnicodeSupported() {
4870
4870
  if (!process2.platform.startsWith("win")) {
@@ -5153,7 +5153,7 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
5153
5153
  var dist_default = figures;
5154
5154
  var replacements = Object.entries(specialMainSymbols);
5155
5155
 
5156
- // ../../node_modules/@inquirer/core/dist/lib/theme.js
5156
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/theme.js
5157
5157
  var defaultTheme = {
5158
5158
  prefix: {
5159
5159
  idle: styleText("blue", "?"),
@@ -5181,7 +5181,7 @@ function getDefaultTheme() {
5181
5181
  };
5182
5182
  }
5183
5183
 
5184
- // ../../node_modules/@inquirer/core/dist/lib/make-theme.js
5184
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/make-theme.js
5185
5185
  function isPlainObject(value) {
5186
5186
  if (typeof value !== "object" || value === null)
5187
5187
  return false;
@@ -5209,7 +5209,7 @@ function makeTheme(...themes) {
5209
5209
  return deepMerge(...themesToMerge);
5210
5210
  }
5211
5211
 
5212
- // ../../node_modules/@inquirer/core/dist/lib/use-prefix.js
5212
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/use-prefix.js
5213
5213
  function usePrefix({ status = "idle", theme }) {
5214
5214
  const [showLoader, setShowLoader] = useState(false);
5215
5215
  const [tick, setTick] = useState(0);
@@ -5239,7 +5239,7 @@ function usePrefix({ status = "idle", theme }) {
5239
5239
  const iconName = status === "loading" ? "idle" : status;
5240
5240
  return typeof prefix === "string" ? prefix : prefix[iconName] ?? prefix["idle"];
5241
5241
  }
5242
- // ../../node_modules/@inquirer/core/dist/lib/use-memo.js
5242
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/use-memo.js
5243
5243
  function useMemo(fn, dependencies) {
5244
5244
  return withPointer((pointer) => {
5245
5245
  const prev = pointer.get();
@@ -5251,12 +5251,12 @@ function useMemo(fn, dependencies) {
5251
5251
  return prev.value;
5252
5252
  });
5253
5253
  }
5254
- // ../../node_modules/@inquirer/core/dist/lib/use-ref.js
5254
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/use-ref.js
5255
5255
  function useRef(val) {
5256
5256
  return useState({ current: val })[0];
5257
5257
  }
5258
5258
 
5259
- // ../../node_modules/@inquirer/core/dist/lib/use-keypress.js
5259
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/use-keypress.js
5260
5260
  function useKeypress(userHandler) {
5261
5261
  const signal = useRef(userHandler);
5262
5262
  signal.current = userHandler;
@@ -5274,10 +5274,10 @@ function useKeypress(userHandler) {
5274
5274
  };
5275
5275
  }, []);
5276
5276
  }
5277
- // ../../node_modules/@inquirer/core/dist/lib/utils.js
5277
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/utils.js
5278
5278
  var import_cli_width = __toESM(require_cli_width(), 1);
5279
5279
 
5280
- // ../../node_modules/fast-string-truncated-width/dist/utils.js
5280
+ // ../../node_modules/.bun/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
5281
5281
  var getCodePointsLength = (() => {
5282
5282
  const SURROGATE_PAIR_RE = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
5283
5283
  return (input) => {
@@ -5296,7 +5296,7 @@ var isWideNotCJKTNotEmoji = (x) => {
5296
5296
  return x === 8987 || x === 9001 || x >= 12272 && x <= 12287 || x >= 12289 && x <= 12350 || x >= 12441 && x <= 12543 || x >= 12549 && x <= 12591 || x >= 12593 && x <= 12686 || x >= 12688 && x <= 12771 || x >= 12783 && x <= 12830 || x >= 12832 && x <= 12871 || x >= 12880 && x <= 19903 || x >= 65040 && x <= 65049 || x >= 65072 && x <= 65106 || x >= 65108 && x <= 65126 || x >= 65128 && x <= 65131 || x >= 127488 && x <= 127490 || x >= 127504 && x <= 127547 || x >= 127552 && x <= 127560 || x >= 131072 && x <= 196605 || x >= 196608 && x <= 262141;
5297
5297
  };
5298
5298
 
5299
- // ../../node_modules/fast-string-truncated-width/dist/index.js
5299
+ // ../../node_modules/.bun/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/index.js
5300
5300
  var ANSI_RE = /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]|\u001b\]8;[^;]*;.*?(?:\u0007|\u001b\u005c)/y;
5301
5301
  var CONTROL_RE = /[\x00-\x08\x0A-\x1F\x7F-\x9F]{1,1000}/y;
5302
5302
  var CJKT_WIDE_RE = /(?:(?![\uFF61-\uFF9F\uFF00-\uFFEF])[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}\p{Script=Tangut}]){1,1000}/yu;
@@ -5395,7 +5395,7 @@ var getStringTruncatedWidth = (input, truncationOptions = {}, widthOptions = {})
5395
5395
  };
5396
5396
  var dist_default2 = getStringTruncatedWidth;
5397
5397
 
5398
- // ../../node_modules/fast-string-width/dist/index.js
5398
+ // ../../node_modules/.bun/fast-string-width@3.0.2/node_modules/fast-string-width/dist/index.js
5399
5399
  var NO_TRUNCATION2 = {
5400
5400
  limit: Infinity,
5401
5401
  ellipsis: "",
@@ -5406,7 +5406,7 @@ var fastStringWidth = (input, options = {}) => {
5406
5406
  };
5407
5407
  var dist_default3 = fastStringWidth;
5408
5408
 
5409
- // ../../node_modules/fast-wrap-ansi/lib/main.js
5409
+ // ../../node_modules/.bun/fast-wrap-ansi@0.2.2/node_modules/fast-wrap-ansi/lib/main.js
5410
5410
  var ESC = "\x1B";
5411
5411
  var CSI = "\x9B";
5412
5412
  var END_CODE = 39;
@@ -5618,7 +5618,7 @@ function wrapAnsi(string, columns, options) {
5618
5618
  `);
5619
5619
  }
5620
5620
 
5621
- // ../../node_modules/@inquirer/core/dist/lib/utils.js
5621
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/utils.js
5622
5622
  function breakLines(content, width) {
5623
5623
  return content.split(`
5624
5624
  `).flatMap((line) => wrapAnsi(line, width, { trim: false, wordWrap: false }).split(`
@@ -5629,7 +5629,7 @@ function readlineWidth() {
5629
5629
  return import_cli_width.default({ defaultWidth: 80, output: readline().output });
5630
5630
  }
5631
5631
 
5632
- // ../../node_modules/@inquirer/core/dist/lib/pagination/use-pagination.js
5632
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/pagination/use-pagination.js
5633
5633
  function usePointerPosition({ active, renderedItems, pageSize, loop }) {
5634
5634
  const state = useRef({
5635
5635
  lastPointer: active,
@@ -5695,12 +5695,12 @@ function usePagination({ items, active, renderItem, pageSize, loop = true }) {
5695
5695
  return pageBuffer.filter((line) => typeof line === "string").join(`
5696
5696
  `);
5697
5697
  }
5698
- // ../../node_modules/@inquirer/core/dist/lib/create-prompt.js
5698
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/create-prompt.js
5699
5699
  var import_mute_stream = __toESM(require_lib(), 1);
5700
5700
  import * as readline2 from "readline";
5701
5701
  import { AsyncResource as AsyncResource3 } from "async_hooks";
5702
5702
 
5703
- // ../../node_modules/signal-exit/dist/mjs/signals.js
5703
+ // ../../node_modules/.bun/signal-exit@4.1.0/node_modules/signal-exit/dist/mjs/signals.js
5704
5704
  var signals = [];
5705
5705
  signals.push("SIGHUP", "SIGINT", "SIGTERM");
5706
5706
  if (process.platform !== "win32") {
@@ -5710,7 +5710,7 @@ if (process.platform === "linux") {
5710
5710
  signals.push("SIGIO", "SIGPOLL", "SIGPWR", "SIGSTKFLT");
5711
5711
  }
5712
5712
 
5713
- // ../../node_modules/signal-exit/dist/mjs/index.js
5713
+ // ../../node_modules/.bun/signal-exit@4.1.0/node_modules/signal-exit/dist/mjs/index.js
5714
5714
  var processOk = (process3) => !!process3 && typeof process3 === "object" && typeof process3.removeListener === "function" && typeof process3.emit === "function" && typeof process3.reallyExit === "function" && typeof process3.listeners === "function" && typeof process3.kill === "function" && typeof process3.pid === "number" && typeof process3.on === "function";
5715
5715
  var kExitEmitter = Symbol.for("signal-exit emitter");
5716
5716
  var global = globalThis;
@@ -5908,10 +5908,10 @@ var {
5908
5908
  unload
5909
5909
  } = signalExitWrap(processOk(process3) ? new SignalExit(process3) : new SignalExitFallback);
5910
5910
 
5911
- // ../../node_modules/@inquirer/core/dist/lib/screen-manager.js
5911
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/screen-manager.js
5912
5912
  import { stripVTControlCharacters } from "util";
5913
5913
 
5914
- // ../../node_modules/@inquirer/ansi/dist/index.js
5914
+ // ../../node_modules/.bun/@inquirer+ansi@2.0.7/node_modules/@inquirer/ansi/dist/index.js
5915
5915
  var ESC2 = "\x1B[";
5916
5916
  var cursorLeft = ESC2 + "G";
5917
5917
  var cursorHide = ESC2 + "?25l";
@@ -5927,7 +5927,7 @@ var cursorTo = (x, y) => {
5927
5927
  var eraseLine = ESC2 + "2K";
5928
5928
  var eraseLines = (lines) => lines > 0 ? (eraseLine + cursorUp(1)).repeat(lines - 1) + eraseLine + cursorLeft : "";
5929
5929
 
5930
- // ../../node_modules/@inquirer/core/dist/lib/screen-manager.js
5930
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/screen-manager.js
5931
5931
  var height = (content) => content.split(`
5932
5932
  `).length;
5933
5933
  var lastLine = (content) => content.split(`
@@ -5993,7 +5993,7 @@ class ScreenManager {
5993
5993
  }
5994
5994
  }
5995
5995
 
5996
- // ../../node_modules/@inquirer/core/dist/lib/promise-polyfill.js
5996
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/promise-polyfill.js
5997
5997
  class PromisePolyfill extends Promise {
5998
5998
  static withResolver() {
5999
5999
  let resolve6;
@@ -6006,7 +6006,7 @@ class PromisePolyfill extends Promise {
6006
6006
  }
6007
6007
  }
6008
6008
 
6009
- // ../../node_modules/@inquirer/core/dist/lib/create-prompt.js
6009
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/create-prompt.js
6010
6010
  import path from "path";
6011
6011
  var nativeSetImmediate = globalThis.setImmediate;
6012
6012
  function getCallSites() {
@@ -6117,7 +6117,7 @@ function createPrompt(view) {
6117
6117
  };
6118
6118
  return prompt;
6119
6119
  }
6120
- // ../../node_modules/@inquirer/core/dist/lib/Separator.js
6120
+ // ../../node_modules/.bun/@inquirer+core@11.2.1+7cb241fc07b679d9/node_modules/@inquirer/core/dist/lib/Separator.js
6121
6121
  import { styleText as styleText2 } from "util";
6122
6122
  class Separator {
6123
6123
  separator = styleText2("dim", Array.from({ length: 15 }).join(dist_default.line));
@@ -6131,7 +6131,7 @@ class Separator {
6131
6131
  return Boolean(choice && typeof choice === "object" && "type" in choice && choice.type === "separator");
6132
6132
  }
6133
6133
  }
6134
- // ../../node_modules/@inquirer/checkbox/dist/index.js
6134
+ // ../../node_modules/.bun/@inquirer+checkbox@5.2.1+7cb241fc07b679d9/node_modules/@inquirer/checkbox/dist/index.js
6135
6135
  import { styleText as styleText3 } from "util";
6136
6136
  var checkboxTheme = {
6137
6137
  icon: {
@@ -8529,7 +8529,8 @@ var recipes = [
8529
8529
  from: "0.7.0",
8530
8530
  to: "0.8.0"
8531
8531
  },
8532
- { id: "starter-0.8.0-to-0.8.1", from: "0.8.0", to: "0.8.1" }
8532
+ { id: "starter-0.8.0-to-0.8.1", from: "0.8.0", to: "0.8.1" },
8533
+ { id: "starter-0.8.1-to-0.8.2", from: "0.8.1", to: "0.8.2" }
8533
8534
  ];
8534
8535
  function resolveRecipeChain(sourceVersion, targetVersion) {
8535
8536
  if (compareVersions2(sourceVersion, minimumSupportedUpdateVersion) < 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "066d8018c0ec0c82fbf01d26013b3b303aaa9c22",
4
- "generatorVersion": "0.8.1",
3
+ "templateCommit": "7c00e145cc8fef9f4c6ec621e8740e3c01f7cc2d",
4
+ "generatorVersion": "0.8.2",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
3
+ from dataclasses import dataclass, replace
4
4
 
5
5
  from .attempt_context_projection import (
6
6
  AttemptContextPreparation,
@@ -15,6 +15,7 @@ from .contracts import JsonValue, LongSessionContext
15
15
  from .engine import EngineRunRequest, PreparedArtifact
16
16
  from .openai_provider import OpenAiCompatibleProvider
17
17
  from .provider_governance import GovernedAttempt, ProviderRequestGovernance
18
+ from .provider_output_guidance import tool_output_messages
18
19
 
19
20
 
20
21
  @dataclass(frozen=True)
@@ -64,6 +65,13 @@ async def govern_provider_attempt(
64
65
  )
65
66
  ),
66
67
  )
68
+ if governed.tools:
69
+ governed = replace(
70
+ governed,
71
+ messages=tool_output_messages(
72
+ governed.messages, governed.output_schema
73
+ ),
74
+ )
67
75
  provider_request = provider.request_body(
68
76
  governed.messages,
69
77
  governed.tools,
@@ -14,6 +14,7 @@ from .config import (
14
14
  from .contracts import AgentErrorCode, JsonValue, RuntimeToolDefinition
15
15
  from .engine import SafeEngineError
16
16
  from .multimodal import response_format
17
+ from .provider_output_guidance import tool_output_messages
17
18
  from .report_types import UsageFacts
18
19
  from .vision_probe import probe_vision
19
20
 
@@ -97,6 +98,9 @@ class OpenAiCompatibleProvider:
97
98
  output_schema: dict[str, JsonValue] | None = None,
98
99
  max_output_tokens: int | None = None,
99
100
  ) -> bytes:
101
+ structured = response_format(output_schema_key, output_schema)
102
+ if tools:
103
+ messages = tool_output_messages(messages, output_schema)
100
104
  payload: dict[str, JsonValue] = {
101
105
  "model": self.config.model,
102
106
  "messages": cast(JsonValue, messages),
@@ -105,8 +109,7 @@ class OpenAiCompatibleProvider:
105
109
  }
106
110
  if tools:
107
111
  payload["tools"] = [tool_schema(tool) for tool in tools]
108
- structured = response_format(output_schema_key, output_schema)
109
- if structured is not None:
112
+ if structured is not None and not tools:
110
113
  payload["response_format"] = structured
111
114
  if max_output_tokens is not None:
112
115
  payload["max_tokens"] = max_output_tokens
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from .contracts import JsonValue
6
+ from .engine import SafeEngineError
7
+
8
+
9
+ def tool_output_messages(
10
+ messages: list[dict[str, JsonValue]],
11
+ output_schema: dict[str, JsonValue] | None,
12
+ ) -> list[dict[str, JsonValue]]:
13
+ """Merge already-governed schema guidance without moving Context source ordinals."""
14
+ if output_schema is None:
15
+ return messages
16
+ if not messages or messages[0].get("role") != "system":
17
+ raise SafeEngineError(
18
+ "INVALID_INPUT", "工具结构化输出缺少系统指令", retryable=False
19
+ )
20
+ instructions = messages[0].get("content")
21
+ if not isinstance(instructions, str):
22
+ raise SafeEngineError(
23
+ "INVALID_INPUT", "工具结构化输出指令无效", retryable=False
24
+ )
25
+ guidance = (
26
+ "\n\n需要工具时,只通过 API 的 function calling 调用已提供的工具,"
27
+ "不要在正文中模拟工具调用。工具处理完成后的最终回复必须是符合以下 "
28
+ "JSON Schema 的单个 JSON 值,不含 Markdown 围栏或额外说明;"
29
+ "工具调用阶段不受最终 JSON 格式限制。\n"
30
+ + json.dumps(output_schema, ensure_ascii=False, separators=(",", ":"))
31
+ )
32
+ if instructions.endswith(guidance):
33
+ return messages
34
+ return [{**messages[0], "content": instructions + guidance}, *messages[1:]]
@@ -124,11 +124,22 @@ class AgentStructuredLogHandler(logging.Handler):
124
124
 
125
125
 
126
126
  def configure_agent_logging() -> None:
127
- """Attach once to package loggers; keep Uvicorn's own access logs separate."""
127
+ """Use one package sink without closing handlers shared with Uvicorn."""
128
128
  logger = logging.getLogger("southwind_agent_server")
129
- if not any(
130
- isinstance(handler, AgentStructuredLogHandler) for handler in logger.handlers
131
- ):
132
- logger.addHandler(AgentStructuredLogHandler())
129
+ structured = next(
130
+ (
131
+ handler
132
+ for handler in logger.handlers
133
+ if isinstance(handler, AgentStructuredLogHandler)
134
+ ),
135
+ None,
136
+ )
137
+ if structured is None:
138
+ structured = AgentStructuredLogHandler()
139
+ for handler in logger.handlers[:]:
140
+ if handler is not structured:
141
+ logger.removeHandler(handler)
142
+ if structured not in logger.handlers:
143
+ logger.addHandler(structured)
133
144
  logger.setLevel(logging.INFO)
134
145
  logger.propagate = False
@@ -4,6 +4,8 @@ import logging
4
4
  from importlib import import_module
5
5
  from typing import TextIO, cast
6
6
 
7
+ import pytest
8
+
7
9
  from southwind_agent_server.structured_logging import (
8
10
  MAX_LOG_BYTES,
9
11
  AgentStructuredLogHandler,
@@ -103,3 +105,38 @@ def test_stdout_failure_does_not_interrupt_runtime() -> None:
103
105
  }
104
106
  )
105
107
  handler.emit(record)
108
+
109
+
110
+ def test_bootstrap_removes_borrowed_sink_without_closing_uvicorn(
111
+ monkeypatch: pytest.MonkeyPatch,
112
+ ) -> None:
113
+ stream = io.StringIO()
114
+ legacy_stream = io.StringIO()
115
+ legacy = logging.StreamHandler(legacy_stream)
116
+ structured = AgentStructuredLogHandler(stream)
117
+ logger = logging.getLogger("southwind_agent_server")
118
+ uvicorn = logging.getLogger("uvicorn")
119
+ monkeypatch.setattr(logger, "handlers", [legacy, structured])
120
+ monkeypatch.setattr(logger, "level", logging.INFO)
121
+ monkeypatch.setattr(logger, "propagate", False)
122
+ monkeypatch.setattr(uvicorn, "handlers", [legacy])
123
+ monkeypatch.setattr(uvicorn, "level", logging.INFO)
124
+ monkeypatch.setattr(uvicorn, "propagate", False)
125
+ closed: list[bool] = []
126
+ monkeypatch.setattr(legacy, "close", lambda: closed.append(True))
127
+
128
+ configure_agent_logging()
129
+ configure_agent_logging()
130
+ assert logger.handlers == [structured]
131
+ assert uvicorn.handlers == [legacy]
132
+ assert not closed
133
+ try:
134
+ raise ValueError("secret-exception")
135
+ except ValueError:
136
+ logger.exception("Agent Run 恢复轮询失败,将继续重试")
137
+ assert len(stream.getvalue().splitlines()) == 1
138
+ assert json.loads(stream.getvalue())["errorCode"] == "AGENT_RUN_RECOVERY_FAILED"
139
+ assert legacy_stream.getvalue() == ""
140
+ assert "secret-exception" not in stream.getvalue()
141
+ uvicorn.warning("independent-server-warning")
142
+ assert "independent-server-warning" in legacy_stream.getvalue()
@@ -0,0 +1,347 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import sqlite3
6
+ from dataclasses import replace
7
+ from pathlib import Path
8
+ from typing import cast
9
+
10
+ import httpx
11
+ import pytest
12
+
13
+ from southwind_agent_server.attempt_context import govern_provider_attempt
14
+ from southwind_agent_server.attempt_context_projection import (
15
+ AttemptContextPreparation,
16
+ governed_envelope,
17
+ )
18
+ from southwind_agent_server.context_message_digest import canonical_json_digest
19
+ from southwind_agent_server.context_version_port import (
20
+ PreparedContext,
21
+ PreparedSource,
22
+ UnavailableContextVersionPort,
23
+ )
24
+ from southwind_agent_server.contracts import JsonValue, ProviderFieldPolicy
25
+ from southwind_agent_server.engine import (
26
+ EngineCompleted,
27
+ EngineRunRequest,
28
+ SafeEngineError,
29
+ ToolCompleted,
30
+ )
31
+ from southwind_agent_server.openai_engine import OpenAiCompatibleEngineAdapter
32
+ from southwind_agent_server.tool_host import ToolHostClient
33
+ from tests.openai_provider_test_support import (
34
+ create_provider,
35
+ governance,
36
+ runtime_request,
37
+ sse_response,
38
+ )
39
+
40
+ SCHEMA: dict[str, JsonValue] = {
41
+ "type": "object",
42
+ "properties": {"total": {"type": "integer"}},
43
+ "required": ["total"],
44
+ "additionalProperties": False,
45
+ }
46
+
47
+
48
+ def structured_request() -> EngineRunRequest:
49
+ request = runtime_request("business.records.list")
50
+ return replace(
51
+ request,
52
+ output_schema_key="business.answer.v1",
53
+ output_schema=SCHEMA,
54
+ provider_data_policy=request.provider_data_policy.model_copy(
55
+ update={
56
+ "envelope_fields": [
57
+ *request.provider_data_policy.envelope_fields,
58
+ ProviderFieldPolicy(
59
+ field="output-schema",
60
+ classification="internal",
61
+ dlp_action="redact",
62
+ ),
63
+ ]
64
+ }
65
+ ),
66
+ )
67
+
68
+
69
+ def test_tool_payload_uses_schema_guidance_without_mutation_or_duplicate() -> None:
70
+ provider = create_provider(httpx.MockTransport(lambda _: httpx.Response(500)))
71
+ request = runtime_request("business.records.list")
72
+ messages: list[dict[str, JsonValue]] = [{"role": "system", "content": "原指令"}]
73
+ body = provider.request_body(messages, request.allowed_tools, "answer.v1", SCHEMA)
74
+ payload = json.loads(body)
75
+ assert "response_format" not in payload
76
+ assert payload["tools"][0]["function"]["name"] == "business.records.list"
77
+ assert payload["messages"][0]["content"].startswith("原指令\n\n")
78
+ assert (
79
+ json.dumps(SCHEMA, ensure_ascii=False, separators=(",", ":"))
80
+ in payload["messages"][0]["content"]
81
+ )
82
+ assert messages[0]["content"] == "原指令"
83
+ assert (
84
+ provider.request_body(
85
+ payload["messages"], request.allowed_tools, "answer.v1", SCHEMA
86
+ )
87
+ == body
88
+ )
89
+ plain = json.loads(provider.request_body(messages, (), "answer.v1", SCHEMA))
90
+ assert plain["messages"] == messages
91
+ assert plain["response_format"]["json_schema"]["strict"] is True
92
+
93
+
94
+ @pytest.mark.parametrize("final", ['{"total":1}', '{"total":"1"}', "```json\n{}\n```"])
95
+ async def test_real_function_round_keeps_strict_final_schema(final: str) -> None:
96
+ turns = 0
97
+ calls = 0
98
+
99
+ async def provider_handler(request: httpx.Request) -> httpx.Response:
100
+ nonlocal turns
101
+ turns += 1
102
+ body = json.loads(request.content)
103
+ assert "response_format" not in body
104
+ assert '"required":["total"]' in body["messages"][0]["content"]
105
+ if turns == 1:
106
+ return sse_response(
107
+ [
108
+ {
109
+ "choices": [
110
+ {
111
+ "delta": {
112
+ "tool_calls": [
113
+ {
114
+ "index": 0,
115
+ "id": "call_1",
116
+ "function": {
117
+ "name": "business.records.list",
118
+ "arguments": "{}",
119
+ },
120
+ }
121
+ ]
122
+ }
123
+ }
124
+ ]
125
+ }
126
+ ]
127
+ )
128
+ assert body["messages"][-1]["role"] == "tool"
129
+ return sse_response([{"choices": [{"delta": {"content": final}}]}])
130
+
131
+ async def tool_handler(request: httpx.Request) -> httpx.Response:
132
+ nonlocal calls
133
+ calls += 1
134
+ assert json.loads(request.content)["executionGrant"] == "grant_1"
135
+ return httpx.Response(200, json={"value": {"total": 1}})
136
+
137
+ engine = OpenAiCompatibleEngineAdapter(
138
+ create_provider(httpx.MockTransport(provider_handler)),
139
+ ToolHostClient("internal-token", transport=httpx.MockTransport(tool_handler)),
140
+ governance(),
141
+ )
142
+ request = structured_request()
143
+ if final == '{"total":1}':
144
+ signals = [signal async for signal in engine.execute(request)]
145
+ assert any(isinstance(signal, ToolCompleted) for signal in signals)
146
+ assert isinstance(signals[-1], EngineCompleted)
147
+ assert signals[-1].output == {"total": 1}
148
+ else:
149
+ with pytest.raises(SafeEngineError) as denied:
150
+ _ = [signal async for signal in engine.execute(request)]
151
+ assert denied.value.code == "OUTPUT_VALIDATION_FAILED"
152
+ assert turns == 2 and calls == 1
153
+
154
+
155
+ async def test_text_tool_call_is_not_executable() -> None:
156
+ async def provider_handler(_: httpx.Request) -> httpx.Response:
157
+ return sse_response(
158
+ [
159
+ {
160
+ "choices": [
161
+ {
162
+ "delta": {
163
+ "content": (
164
+ '{"tool_calls":[{"name":"business.records.list",'
165
+ '"arguments":{}}]}'
166
+ )
167
+ }
168
+ }
169
+ ]
170
+ }
171
+ ]
172
+ )
173
+
174
+ async def tool_handler(_: httpx.Request) -> httpx.Response:
175
+ pytest.fail("正文中的工具调用不得执行")
176
+
177
+ engine = OpenAiCompatibleEngineAdapter(
178
+ create_provider(httpx.MockTransport(provider_handler)),
179
+ ToolHostClient("internal-token", transport=httpx.MockTransport(tool_handler)),
180
+ governance(),
181
+ )
182
+ with pytest.raises(SafeEngineError) as denied:
183
+ _ = [signal async for signal in engine.execute(structured_request())]
184
+ assert denied.value.code == "OUTPUT_VALIDATION_FAILED"
185
+
186
+
187
+ async def test_governed_guidance_matches_audit_and_context_bytes(
188
+ tmp_path: Path,
189
+ ) -> None:
190
+ request = structured_request()
191
+ request = replace(
192
+ request, output_schema={**SCHEMA, "description": "password=secret"}
193
+ )
194
+ provider = create_provider(httpx.MockTransport(lambda _: httpx.Response(500)))
195
+ audit_path = str(tmp_path / "audit.sqlite3")
196
+ messages: list[dict[str, JsonValue]] = [
197
+ {"role": "system", "content": request.instructions},
198
+ {"role": "user", "content": "历史来源"},
199
+ {"role": "user", "content": request.user_prompt},
200
+ ]
201
+ prepared = await govern_provider_attempt(
202
+ UnavailableContextVersionPort(),
203
+ governance(audit_path),
204
+ provider,
205
+ request,
206
+ 1,
207
+ None,
208
+ messages,
209
+ )
210
+ payload = json.loads(prepared.provider_request)
211
+ assert payload["messages"] == prepared.governed.messages
212
+ assert "password=secret" not in prepared.provider_request.decode()
213
+ assert "[REDACTED]" in payload["messages"][0]["content"]
214
+ with sqlite3.connect(audit_path) as connection:
215
+ assert connection.execute(
216
+ "select sent_bytes, decision from provider_attempt_audit"
217
+ ).fetchone() == (len(prepared.provider_request), "redact")
218
+ source = PreparedSource(
219
+ "business", "session", "source-1", "v1", 1, "a" * 64, {}, "历史来源", "user"
220
+ )
221
+ envelope = governed_envelope(
222
+ AttemptContextPreparation(
223
+ PreparedContext("preparation-1", (source,)), messages, 1
224
+ ),
225
+ request.run_id,
226
+ "attempt-1",
227
+ prepared.governed.messages,
228
+ prepared.provider_request,
229
+ provider.config.deployment_key,
230
+ provider.config.model,
231
+ request.prompt_version,
232
+ (),
233
+ )
234
+ entries = cast(list[dict[str, JsonValue]], envelope["messages"])
235
+ assert entries[0]["content"] == payload["messages"][0]["content"]
236
+ assert (
237
+ cast(list[dict[str, JsonValue]], entries[1]["origins"])[0]["sourceId"]
238
+ == "source-1"
239
+ )
240
+ provider_fields = cast(dict[str, JsonValue], envelope["provider"])
241
+ assert (
242
+ provider_fields["requestDigest"]
243
+ == hashlib.sha256(prepared.provider_request).hexdigest()
244
+ )
245
+ assert provider_fields["outputSchemaDigest"] == hashlib.sha256(b"null").hexdigest()
246
+ assert entries[0]["digest"] == canonical_json_digest(payload["messages"][0])
247
+ assert entries[0]["digest"] != canonical_json_digest(cast(JsonValue, messages[0]))
248
+
249
+
250
+ async def test_schema_guidance_still_requires_output_schema_authorization() -> None:
251
+ request = structured_request()
252
+ request = replace(
253
+ request,
254
+ provider_data_policy=runtime_request(
255
+ "business.records.list"
256
+ ).provider_data_policy,
257
+ )
258
+ with pytest.raises(SafeEngineError) as denied:
259
+ await govern_provider_attempt(
260
+ UnavailableContextVersionPort(),
261
+ governance(),
262
+ create_provider(httpx.MockTransport(lambda _: httpx.Response(500))),
263
+ request,
264
+ 1,
265
+ None,
266
+ [{"role": "system", "content": "指令"}],
267
+ )
268
+ assert denied.value.code == "FORBIDDEN"
269
+
270
+
271
+ async def test_schema_guidance_counts_toward_input_token_budget() -> None:
272
+ calls = 0
273
+
274
+ async def handler(_: httpx.Request) -> httpx.Response:
275
+ nonlocal calls
276
+ calls += 1
277
+ return sse_response([])
278
+
279
+ provider = create_provider(httpx.MockTransport(handler))
280
+ request = structured_request()
281
+ messages: list[dict[str, JsonValue]] = [
282
+ {"role": "system", "content": request.instructions},
283
+ {"role": "user", "content": request.user_prompt},
284
+ ]
285
+ original_bytes = len(provider.request_body(messages, request.allowed_tools))
286
+ assert original_bytes < 1024
287
+ request = replace(
288
+ request,
289
+ output_schema={**SCHEMA, "description": "业务结果字段说明。" * 100},
290
+ limits=request.limits.model_copy(update={"max_input_tokens": original_bytes}),
291
+ )
292
+ engine = OpenAiCompatibleEngineAdapter(
293
+ provider, ToolHostClient("internal-token"), governance()
294
+ )
295
+ with pytest.raises(SafeEngineError) as denied:
296
+ _ = [signal async for signal in engine.execute(request)]
297
+ assert denied.value.code == "BUDGET_EXCEEDED"
298
+ assert calls == 0
299
+
300
+
301
+ async def test_tool_schema_respects_model_attempt_limit() -> None:
302
+ calls = 0
303
+
304
+ async def handler(_: httpx.Request) -> httpx.Response:
305
+ nonlocal calls
306
+ calls += 1
307
+ return sse_response(
308
+ [
309
+ {
310
+ "choices": [
311
+ {
312
+ "delta": {
313
+ "tool_calls": [
314
+ {
315
+ "index": 0,
316
+ "id": "call_1",
317
+ "function": {
318
+ "name": "business.records.list",
319
+ "arguments": "{}",
320
+ },
321
+ }
322
+ ]
323
+ }
324
+ }
325
+ ]
326
+ }
327
+ ]
328
+ )
329
+
330
+ request = structured_request()
331
+ request = replace(
332
+ request, limits=request.limits.model_copy(update={"max_model_attempts": 1})
333
+ )
334
+ engine = OpenAiCompatibleEngineAdapter(
335
+ create_provider(httpx.MockTransport(handler)),
336
+ ToolHostClient(
337
+ "internal-token",
338
+ transport=httpx.MockTransport(
339
+ lambda _: httpx.Response(200, json={"value": {"total": 1}})
340
+ ),
341
+ ),
342
+ governance(),
343
+ )
344
+ with pytest.raises(SafeEngineError) as denied:
345
+ _ = [signal async for signal in engine.execute(request)]
346
+ assert denied.value.code == "BUDGET_EXCEEDED"
347
+ assert calls == 1
@@ -1,6 +1,7 @@
1
1
  # 结构化运行日志技术实现
2
2
 
3
- 状态:SDK、平台适配与利剑四条链的本地定向联调已实现;正式发布/升级及集中查询/告警平台未验收。
3
+ 状态:SDK 与 E1 已随 Windy `0.8.1` 正式发布,利剑完成官方更新和正式 SDK 四条链定向联调;
4
+ 完整隔离执行、真实 Provider、生产采集留存及集中查询/告警平台未验收。
4
5
  对应业务方案见
5
6
  [日志治理方案](./log-governance-plan.md)。
6
7
 
@@ -75,5 +76,21 @@
75
76
  容器依赖层安装和已有静态失败在利剑最终文档分别记录。
76
77
  候选 SDK 用真实本地归档消费,没有改 `.windy` 元数据或手改版本号冒充正式升级。
77
78
 
78
- 本轮完成日志契约与四条真实适配链的本地定向联调;未完成真实 Provider/完整 Agent 部署、正式
79
- Windy 发布和 `windy update` 事务、生产采集留存、集中查询或告警验收。这些不能由定向检查替代。
79
+ ## 正式发布与利剑升级
80
+
81
+ 2026-10-01 核验 `create-windy@0.8.1`、`@southwind-ai/shared@0.3.1` 已在 npm 发布,
82
+ `create-windy-v0.8.1` tag 指向 `066d8018c0ec0c82fbf01d26013b3b303aaa9c22`。
83
+ [发布记录](https://github.com/RayJason/windy-platform/releases/tag/create-windy-v0.8.1) 已附
84
+ candidate/published evidence、distribution manifest 与 checksum;两次发布核验均成功。
85
+ 利剑官方更新账本绑定同一 SHA 和 `0.8.1`,当前使用正常的
86
+ `@southwind-ai/shared/structured-log`,已删除候选别名及归档。详细更新及验收记录在利剑
87
+ `docs/operations/windy-logging-integration.md`。
88
+
89
+ 未完成 E2—E4 集成发布、真实 Provider/完整 Agent 部署、生产采集留存、集中查询或告警验收。
90
+ 日志 SDK 正式接入不能当作完整 Agent 能力已经交付的证据。
91
+
92
+ 利剑合并验收另发现旧 Bootstrap 借用 Uvicorn Handler 后再追加 SDK 会形成双出口。
93
+ `configure_agent_logging()` 已回馈上游:保留唯一现存结构化 Handler,移除包级其它 Handler 引用,
94
+ 不关闭与 Uvicorn 共享的 Handler;重复初始化保留原实例和 Stream。5 项适配定向检查通过。
95
+ 该修复是 `0.8.1` 发布后的源码补丁,没有改写发布版本;利剑同时修复旧 Bootstrap 调用点,并用
96
+ 真实 Uvicorn 配置先于 ASGI 导入的回归检查证明单条 JSON 输出及独立访问日志仍有效。
@@ -1,7 +1,7 @@
1
1
  # 日志治理方案
2
2
 
3
- 状态:运行日志 SDK、平台适配及利剑四条链的本地定向联调已实现;正式发布、下游升级与部署留存
4
- 另行验收。集中管理界面尚未实施。
3
+ 状态:运行日志 SDK 和平台适配随 Windy `0.8.1` 正式发布,利剑已通过官方更新接入并完成四条链
4
+ 定向联调。部署留存与真实 Provider 另行验收;集中管理界面尚未实施。
5
5
 
6
6
  ## 业务需求先行
7
7
 
@@ -52,6 +52,24 @@ Provider 调用前会执行:
52
52
  Tool 调用继续由 Windy Server 执行。Agent Server 只收到 opaque Execution Grant;每次 Tool
53
53
  都重新校验当前权限、Scope、审批和 receipt,不把 Run 创建时的权限快照当长期授权。
54
54
 
55
+ ### Tool 与结构化业务结果
56
+
57
+ 需要先查询业务数据再返回结构化结果的 Operation,同时保留真实 function calling 与最终
58
+ JSON Schema 验证。为兼容同时发送 `tools` 和 `response_format` 时只生成正文的 Provider,
59
+ 存在 Tool 时请求只发送 `tools`,把经过授权和 DLP 的输出 Schema 及最终 JSON 指导合并到
60
+ 首条系统指令;没有 Tool 时继续发送严格 `response_format: json_schema`。这是默认序列化
61
+ 规则,无新增配置。正文中的工具名称或伪造调用不执行,只有协议中的 `tool_calls` 执行。
62
+
63
+ 合并不增加消息、移动 Context 来源序号或放宽 Guard。治理后的同一消息用于 Context 提交、
64
+ 输入预算和实际出网请求;Schema 指导也计入 Provider Context 与保守 Token 预算、出网审计
65
+ 字节数。`maxInputBytes` 继续约束原始输入和 Artifact,原有输出预算与有界 Tool 轮次继续生效。
66
+ 最终回复仍必须通过严格 Schema 验证,
67
+ 不接受 Markdown 围栏或额外字段;Provider 未生成合规结果时返回 `OUTPUT_VALIDATION_FAILED`。
68
+
69
+ Context 的首条指令摘要与 `requestDigest` 覆盖完整、脱敏后的 Schema 指导。兼容请求没有
70
+ `response_format`,因此 `outputSchemaDigest` 是实际空字段的摘要;它不能单独用来恢复
71
+ 输出 Schema。首条指令的来源与版本规则保持不变,业务来源仍按原始序号追踪。
72
+
55
73
  ## 配置
56
74
 
57
75
  选择 `system.agent` 的项目在本机 `.env` 配置: