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 +35 -34
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/src/southwind_agent_server/attempt_context.py +9 -1
- package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +5 -2
- package/template/apps/agent-server/src/southwind_agent_server/provider_output_guidance.py +34 -0
- package/template/apps/agent-server/src/southwind_agent_server/structured_logging.py +16 -5
- package/template/apps/agent-server/tests/test_structured_logging.py +37 -0
- package/template/apps/agent-server/tests/test_tool_structured_output.py +347 -0
- package/template/docs/architecture/log-governance-implementation.md +20 -3
- package/template/docs/architecture/log-governance-plan.md +2 -2
- package/template/docs/platform/agent-runtime.md +18 -0
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
|
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
|
|
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
|
-
"""
|
|
127
|
+
"""Use one package sink without closing handlers shared with Uvicorn."""
|
|
128
128
|
logger = logging.getLogger("southwind_agent_server")
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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 输出及独立访问日志仍有效。
|
|
@@ -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` 配置:
|