@roll-agent/core 0.18.0 → 0.19.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.
- package/THIRD_PARTY_NOTICES.txt +112 -0
- package/dist/cli/chat/banner.d.ts +13 -0
- package/dist/cli/chat/banner.js +1 -1
- package/dist/cli/chat/ink/app.d.ts +3 -0
- package/dist/cli/chat/ink/app.js +1 -1
- package/dist/cli/chat/ink/banner-view.d.ts +15 -0
- package/dist/cli/chat/ink/banner-view.js +1 -0
- package/dist/cli/chat/ink/history-item.js +1 -1
- package/dist/cli/chat/ink/run-ink-repl.js +1 -1
- package/dist/cli/commands/config-explain.js +1 -1
- package/dist/cli/commands/config-setup.js +1 -1
- package/dist/cli/commands/config.js +1 -1
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/ui.d.ts +30 -0
- package/dist/cli/commands/ui.js +1 -0
- package/dist/cli/index.js +1 -1
- package/dist/config/application-service.d.ts +90 -0
- package/dist/config/application-service.js +1 -0
- package/dist/config/catalog.d.ts +76 -0
- package/dist/config/catalog.js +1 -0
- package/dist/config/defaults.d.ts +2 -2
- package/dist/config/defaults.js +1 -1
- package/dist/config/document-store.d.ts +70 -0
- package/dist/config/document-store.js +1 -0
- package/dist/config/guidance.d.ts +549 -0
- package/dist/config/guidance.js +1 -0
- package/dist/config/index.d.ts +6 -0
- package/dist/config/index.js +1 -1
- package/dist/config/key-codec.d.ts +24 -3
- package/dist/config/key-codec.js +1 -1
- package/dist/config/migration.js +1 -1
- package/dist/config/secret-policy.d.ts +20 -0
- package/dist/config/secret-policy.js +1 -0
- package/dist/llm/providers.d.ts +2 -0
- package/dist/llm/providers.js +1 -1
- package/dist/registry/agent-lifecycle.d.ts +97 -0
- package/dist/registry/agent-lifecycle.js +1 -0
- package/dist/registry/discovery.js +1 -1
- package/dist/registry/process-identity.d.ts +14 -0
- package/dist/registry/process-identity.js +1 -0
- package/dist/registry/process-manager.d.ts +28 -7
- package/dist/registry/process-manager.js +1 -1
- package/dist/registry/store.js +1 -1
- package/dist/types/agent.d.ts +6 -0
- package/dist/types/agent.js +1 -1
- package/dist/ui/config-controller.d.ts +15 -0
- package/dist/ui/config-controller.js +1 -0
- package/dist/ui/contracts.d.ts +49 -0
- package/dist/ui/contracts.js +1 -0
- package/dist/ui/index.d.ts +8 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/runtime-controller.d.ts +38 -0
- package/dist/ui/runtime-controller.js +1 -0
- package/dist/ui/server.d.ts +21 -0
- package/dist/ui/server.js +1 -0
- package/dist/ui/static-assets.d.ts +2 -0
- package/dist/ui/static-assets.js +1 -0
- package/dist/ui-assets/assets/app.css +1 -0
- package/dist/ui-assets/assets/app.js +53 -0
- package/dist/ui-assets/assets/roll-mark-wXIkY5y0.svg +5 -0
- package/dist/ui-assets/index.html +17 -0
- package/package.json +15 -8
- package/dist/cli/commands/config-guidance.d.ts +0 -148
- package/dist/cli/commands/config-guidance.js +0 -1
package/dist/config/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{rollConfigSchema}from"./schema.js";export{loadConfig}from"./loader.js";export{DEFAULT_CONFIG,CONFIG_FILE_NAMES}from"./defaults.js";export{getAgentEnv}from"./helpers.js";
|
|
1
|
+
export{rollConfigSchema}from"./schema.js";export{loadConfig}from"./loader.js";export{DEFAULT_CONFIG,CONFIG_FILE_NAMES}from"./defaults.js";export{getAgentEnv}from"./helpers.js";export{CONFIG_UI_SECRET_SENTINEL,ConfigApplicationService,ConfigApplicationValidationError,createConfigPatches,planConfigActivation}from"./application-service.js";export{ConfigDocumentParseError,ConfigRevisionConflictError,YamlConfigDocumentStore,createConfigRevision}from"./document-store.js";export{buildRollConfigCatalog}from"./catalog.js";
|
|
@@ -1,15 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
export type KeyCodecNode = {
|
|
2
3
|
readonly kind: "object";
|
|
3
4
|
readonly fields: Readonly<Record<string, KeyCodecNode>>;
|
|
4
5
|
} | {
|
|
5
6
|
readonly kind: "record";
|
|
6
7
|
readonly value: KeyCodecNode;
|
|
8
|
+
} | {
|
|
9
|
+
readonly kind: "array";
|
|
10
|
+
readonly item: KeyCodecNode;
|
|
7
11
|
} | {
|
|
8
12
|
readonly kind: "leaf";
|
|
9
13
|
};
|
|
10
|
-
|
|
14
|
+
/**
|
|
15
|
+
* 从 Zod schema 派生 YAML key 编码树。
|
|
16
|
+
*
|
|
17
|
+
* object 字段使用 kebab-case;record 的动态 key 和 array 的索引保持原样。
|
|
18
|
+
* 这让新增普通 Roll 配置字段无需再维护第二份 key 清单。
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildKeyCodecNode(schema: z.ZodTypeAny): KeyCodecNode;
|
|
21
|
+
export declare const CONFIG_KEY_CODEC: {
|
|
22
|
+
readonly kind: "object";
|
|
23
|
+
readonly fields: Readonly<Record<string, KeyCodecNode>>;
|
|
24
|
+
};
|
|
11
25
|
export declare function kebabToCamel(key: string): string;
|
|
12
26
|
export declare function camelToKebab(key: string): string;
|
|
13
27
|
export declare function decodeFromYaml(value: unknown, node?: KeyCodecNode): unknown;
|
|
14
|
-
|
|
15
|
-
|
|
28
|
+
/**
|
|
29
|
+
* 将运行时使用的 camelCase 配置对象编码为持久化 YAML 使用的 kebab-case 结构。
|
|
30
|
+
*
|
|
31
|
+
* `record` 节点的用户键必须原样保留,例如 provider 名、Agent 名以及
|
|
32
|
+
* `runtime.approval.overrides` 中可能包含英文句点的完整 tool name。
|
|
33
|
+
*/
|
|
34
|
+
export declare function encodeToYaml(value: unknown, node?: KeyCodecNode): unknown;
|
|
35
|
+
export declare function encodePathToYaml(parts: readonly string[], node?: KeyCodecNode): string[];
|
|
36
|
+
export declare function normalizeUserPath(parts: readonly string[], node?: KeyCodecNode): string[];
|
package/dist/config/key-codec.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
import{z as e}from"zod";import{rollConfigSchema as o}from"./schema.js";const n={kind:"leaf"};export function buildKeyCodecNode(o){const t=r(o);if(t instanceof e.ZodObject){return{kind:"object",fields:Object.fromEntries(Object.entries(t.shape).map(([o,n])=>{if(!(n instanceof e.ZodType))throw new Error(`Unsupported Zod object field: ${o}`);return[o,buildKeyCodecNode(n)]}))}}return t instanceof e.ZodRecord?{kind:"record",value:buildKeyCodecNode(t.valueSchema)}:t instanceof e.ZodArray?{kind:"array",item:buildKeyCodecNode(t.element)}:n}function r(o){return o instanceof e.ZodOptional||o instanceof e.ZodNullable?r(o.unwrap()):o instanceof e.ZodDefault?r(o.removeDefault()):o instanceof e.ZodEffects?r(o.innerType()):o instanceof e.ZodCatch?r(o.removeCatch()):o instanceof e.ZodBranded?r(o.unwrap()):o}function t(){const e=buildKeyCodecNode(o);if("object"!==e.kind)throw new Error("rollConfigSchema root must be an object");return e}export const CONFIG_KEY_CODEC=t();export function kebabToCamel(e){return e.replace(/-([a-z])/g,(e,o)=>o.toUpperCase())}export function camelToKebab(e){return e.replace(/[A-Z]/g,e=>`-${e.toLowerCase()}`)}function c(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}export function decodeFromYaml(e,o=CONFIG_KEY_CODEC){if(Array.isArray(e)){const n="array"===o.kind?o.item:o;return e.map(e=>decodeFromYaml(e,n))}if(!c(e))return e;if("object"===o.kind){const r={};for(const[t,c]of Object.entries(e)){const e=kebabToCamel(t),i=o.fields[e]??n;r[e]=decodeFromYaml(c,i)}return r}if("record"===o.kind){const n={};for(const[r,t]of Object.entries(e))n[r]=decodeFromYaml(t,o.value);return n}return e}export function encodeToYaml(e,o=CONFIG_KEY_CODEC){if(Array.isArray(e)){const n="array"===o.kind?o.item:o;return e.map(e=>encodeToYaml(e,n))}if(!c(e))return e;if("object"===o.kind){const r={};for(const[t,c]of Object.entries(e)){const e=o.fields[t]??n;r[camelToKebab(t)]=encodeToYaml(c,e)}return r}if("record"===o.kind){const n={};for(const[r,t]of Object.entries(e))n[r]=encodeToYaml(t,o.value);return n}return e}function i(e,o,r){const t=[];let c=r;for(let r=0;r<e.length;r+=1){const i=e[r];if("object"===c.kind){const e=kebabToCamel(i),r=c.fields[e];r?(t.push("camel"===o?e:camelToKebab(e)),c=r):(t.push(i),c=n)}else if("record"===c.kind){const o=c.value;if("leaf"===o.kind){t.push(e.slice(r).join("."));break}if("object"===o.kind){const n=e.findIndex((e,n)=>n>r&&void 0!==o.fields[kebabToCamel(e)]);if(n>r){t.push(e.slice(r,n).join(".")),c=o,r=n-1;continue}}t.push(i),c=o}else"array"===c.kind?(t.push(i),c=c.item):t.push(i)}return t}export function encodePathToYaml(e,o=CONFIG_KEY_CODEC){return i(e,"kebab",o)}export function normalizeUserPath(e,o=CONFIG_KEY_CODEC){return i(e,"camel",o)}
|
package/dist/config/migration.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{camelToKebab as e,CONFIG_KEY_CODEC as t,kebabToCamel as s}from"./key-codec.js";const n={deprecatedRouterSection:"deprecated-router-section",invalidAskSection:"invalid-ask-section",invalidRouterSection:"invalid-router-section",routerAskConflict:"router-ask-conflict",duplicateEquivalentKeys:"duplicate-equivalent-keys",unknownRouterKeys:"unknown-router-keys",legacyCamelCaseAgentEnvKey:"legacy-camelcase-agent-env-key",legacyAgentEnvKeyConflict:"legacy-agent-env-key-conflict",deprecatedRuntimeBashSection:"deprecated-runtime-bash-section",invalidRuntimeBashSection:"invalid-runtime-bash-section",invalidRuntimeShellSection:"invalid-runtime-shell-section",runtimeShellConflict:"runtime-shell-conflict"},o={llmModel:{routerKeys:["llm-model","llmModel"],askKeys:["llm-model","llmModel"],targetAskKey:"llm-model"},confirmThreshold:{routerKeys:["confirm-threshold","confirmThreshold"],askKeys:["confirm-threshold","confirmThreshold"],targetAskKey:"confirm-threshold"}},r=[...o.llmModel.routerKeys,...o.confirmThreshold.routerKeys,"mode"],i=new Set([n.invalidAskSection,n.invalidRouterSection,n.routerAskConflict,n.unknownRouterKeys,n.legacyAgentEnvKeyConflict,n.invalidRuntimeBashSection,n.invalidRuntimeShellSection,n.runtimeShellConflict]);function u(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function c(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function a(e,t){return{code:e,message:t}}function l(e,t){if(Object.is(e,t))return!0;if(!u(e)||!u(t))return!1;const s=Object.keys(e).sort(),n=Object.keys(t).sort();return s.length===n.length&&s.every((s,o)=>s===n[o]&&l(e[s],t[s]))}function d(){if("object"!==t.kind)throw new Error("runtime shell key codec is unavailable");const e=t.fields.runtime;if("object"!==e?.kind)throw new Error("runtime shell key codec is unavailable");const s=e.fields.shell;if("object"!==s?.kind)throw new Error("runtime shell key codec is unavailable");return s}function h(e,t,n){if(!u(e)||"leaf"===t.kind)return{ok:!0,value:e};const o={};for(const[r,i]of Object.entries(e)){const e="object"===t.kind?s(r):r,u="object"===t.kind?t.fields[e]:t.value,a=void 0===u?{ok:!0,value:i}:h(i,u,[...n,e]);if(!a.ok)return a;if(c(o,e)){if(!l(o[e],a.value))return{ok:!1,conflictPath:[...n,e].join(".")}}else o[e]=a.value}return{ok:!0,value:o}}function f(e,t){const s=h(e,d(),[t]);return s.ok?u(s.value)?{ok:!0,value:s.value}:{ok:!0,value:e}:s}function m(e,t){if(!u(e)||"leaf"===t.kind)return e;const n={},o=new Set;for(const[r,i]of Object.entries(e)){const e="object"===t.kind?s(r):r;if(o.has(e))continue;o.add(e);const u="object"===t.kind?t.fields[e]:t.value;n[r]=void 0===u?i:m(i,u)}return n}function k(e,t){return t.filter(t=>c(e,t))}function g(e,t,s){const o=k(t,s);if(0===o.length)return{ok:!0,key:void 0,value:void 0};const[r]=o,i=r?t[r]:void 0;return o.some(e=>t[e]!==i)?{ok:!1,issue:a(n.duplicateEquivalentKeys,`\`${e}\` 同时包含等价键 ${o.join(", ")} 且值不一致,请手动处理。`)}:{ok:!0,key:r,value:i}}function y(e){if(!c(e,"router"))return{matches:!1,canAutoMigrate:!1,issues:[]};const t=[a(n.deprecatedRouterSection,"`router` 配置段已废弃,请改用 `ask`。"),a(n.deprecatedRouterSection,"将 `router.llm-model` 迁移为 `ask.llm-model`。"),a(n.deprecatedRouterSection,"将 `router.confirm-threshold` 迁移为 `ask.confirm-threshold`。"),a(n.deprecatedRouterSection,"删除 `router.mode`;命令本身已决定策略(`run` / `ask` / `chat`)。")],s=e.router;if(!u(s))return t.push(a(n.invalidRouterSection,"`router` 配置段不是对象,无法自动迁移,请手动修复。")),{matches:!0,canAutoMigrate:!1,issues:t};const l=Object.keys(s).filter(e=>!r.includes(e));l.length>0&&t.push(a(n.unknownRouterKeys,`\`router\` 包含无法自动迁移的未知键:${l.join(", ")}。`));const d=e.ask;if(void 0===d||u(d)||t.push(a(n.invalidAskSection,"`ask` 配置段不是对象,无法自动迁移,请手动修复。")),u(d))for(const e of Object.keys(o)){const r=o[e],i=g("router",s,r.routerKeys),u=g("ask",d,r.askKeys);i.ok?u.ok?i.key&&u.key&&u.value!==i.value&&t.push(a(n.routerAskConflict,`\`router.${r.targetAskKey}\` 与 \`ask.${r.targetAskKey}\` 同时存在且值冲突,请手动处理。`)):t.push(u.issue):t.push(i.issue)}return{matches:!0,canAutoMigrate:!t.some(e=>i.has(e.code)),issues:t}}function v(e){const t=y(e);if(!t.matches)return{ok:!0,changed:!1,document:structuredClone(e),issues:[],summary:[]};if(!t.canAutoMigrate)return{ok:!1,changed:!1,issues:t.issues};const s=structuredClone(e),n=s.router;if(!u(n))return{ok:!1,changed:!1,issues:t.issues};const r=s.ask,i=u(r)?r:{};let a=!1;const l=[];for(const e of Object.keys(o)){const t=o[e],s=g("router",n,t.routerKeys);if(!s.ok)return{ok:!1,changed:!1,issues:[s.issue]};if(!s.key)continue;const r=g("ask",i,t.askKeys);if(!r.ok)return{ok:!1,changed:!1,issues:[r.issue]};r.key?l.push(`移除已废弃的 \`router.${t.targetAskKey}\``):(i[t.targetAskKey]=s.value,a=!0,l.push(`将 \`router.${t.targetAskKey}\` 迁移到 \`ask.${t.targetAskKey}\``));for(const e of t.routerKeys)c(n,e)&&delete n[e]}return c(n,"mode")&&(delete n.mode,l.push("删除已废弃的 `router.mode`")),a&&(s.ask=i),0===Object.keys(n).length&&(delete s.router,l.push("删除空的 `router` 配置段")),{ok:!0,changed:l.length>0,document:s,issues:t.issues,summary:l}}const p=/^[a-z0-9]+(-[a-z0-9]+)*$/;function
|
|
1
|
+
import{camelToKebab as e,CONFIG_KEY_CODEC as t,kebabToCamel as s}from"./key-codec.js";const n={deprecatedRouterSection:"deprecated-router-section",invalidAskSection:"invalid-ask-section",invalidRouterSection:"invalid-router-section",routerAskConflict:"router-ask-conflict",duplicateEquivalentKeys:"duplicate-equivalent-keys",unknownRouterKeys:"unknown-router-keys",legacyCamelCaseAgentEnvKey:"legacy-camelcase-agent-env-key",legacyAgentEnvKeyConflict:"legacy-agent-env-key-conflict",deprecatedRuntimeBashSection:"deprecated-runtime-bash-section",invalidRuntimeBashSection:"invalid-runtime-bash-section",invalidRuntimeShellSection:"invalid-runtime-shell-section",runtimeShellConflict:"runtime-shell-conflict"},o={llmModel:{routerKeys:["llm-model","llmModel"],askKeys:["llm-model","llmModel"],targetAskKey:"llm-model"},confirmThreshold:{routerKeys:["confirm-threshold","confirmThreshold"],askKeys:["confirm-threshold","confirmThreshold"],targetAskKey:"confirm-threshold"}},r=[...o.llmModel.routerKeys,...o.confirmThreshold.routerKeys,"mode"],i=new Set([n.invalidAskSection,n.invalidRouterSection,n.routerAskConflict,n.unknownRouterKeys,n.legacyAgentEnvKeyConflict,n.invalidRuntimeBashSection,n.invalidRuntimeShellSection,n.runtimeShellConflict]);function u(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}function c(e,t){return Object.prototype.hasOwnProperty.call(e,t)}function a(e,t){return{code:e,message:t}}function l(e,t){if(Object.is(e,t))return!0;if(!u(e)||!u(t))return!1;const s=Object.keys(e).sort(),n=Object.keys(t).sort();return s.length===n.length&&s.every((s,o)=>s===n[o]&&l(e[s],t[s]))}function d(){if("object"!==t.kind)throw new Error("runtime shell key codec is unavailable");const e=t.fields.runtime;if("object"!==e?.kind)throw new Error("runtime shell key codec is unavailable");const s=e.fields.shell;if("object"!==s?.kind)throw new Error("runtime shell key codec is unavailable");return s}function h(e,t,n){if(Array.isArray(e)&&"array"===t.kind){const s=[];for(const[o,r]of e.entries()){const e=h(r,t.item,[...n,String(o)]);if(!e.ok)return e;s.push(e.value)}return{ok:!0,value:s}}if(!u(e)||"leaf"===t.kind||"array"===t.kind)return{ok:!0,value:e};const o={};for(const[r,i]of Object.entries(e)){const e="object"===t.kind?s(r):r,u="object"===t.kind?t.fields[e]:t.value,a=void 0===u?{ok:!0,value:i}:h(i,u,[...n,e]);if(!a.ok)return a;if(c(o,e)){if(!l(o[e],a.value))return{ok:!1,conflictPath:[...n,e].join(".")}}else o[e]=a.value}return{ok:!0,value:o}}function f(e,t){const s=h(e,d(),[t]);return s.ok?u(s.value)?{ok:!0,value:s.value}:{ok:!0,value:e}:s}function m(e,t){if(Array.isArray(e)&&"array"===t.kind)return e.map(e=>m(e,t.item));if(!u(e)||"leaf"===t.kind||"array"===t.kind)return e;const n={},o=new Set;for(const[r,i]of Object.entries(e)){const e="object"===t.kind?s(r):r;if(o.has(e))continue;o.add(e);const u="object"===t.kind?t.fields[e]:t.value;n[r]=void 0===u?i:m(i,u)}return n}function k(e,t){return t.filter(t=>c(e,t))}function g(e,t,s){const o=k(t,s);if(0===o.length)return{ok:!0,key:void 0,value:void 0};const[r]=o,i=r?t[r]:void 0;return o.some(e=>t[e]!==i)?{ok:!1,issue:a(n.duplicateEquivalentKeys,`\`${e}\` 同时包含等价键 ${o.join(", ")} 且值不一致,请手动处理。`)}:{ok:!0,key:r,value:i}}function y(e){if(!c(e,"router"))return{matches:!1,canAutoMigrate:!1,issues:[]};const t=[a(n.deprecatedRouterSection,"`router` 配置段已废弃,请改用 `ask`。"),a(n.deprecatedRouterSection,"将 `router.llm-model` 迁移为 `ask.llm-model`。"),a(n.deprecatedRouterSection,"将 `router.confirm-threshold` 迁移为 `ask.confirm-threshold`。"),a(n.deprecatedRouterSection,"删除 `router.mode`;命令本身已决定策略(`run` / `ask` / `chat`)。")],s=e.router;if(!u(s))return t.push(a(n.invalidRouterSection,"`router` 配置段不是对象,无法自动迁移,请手动修复。")),{matches:!0,canAutoMigrate:!1,issues:t};const l=Object.keys(s).filter(e=>!r.includes(e));l.length>0&&t.push(a(n.unknownRouterKeys,`\`router\` 包含无法自动迁移的未知键:${l.join(", ")}。`));const d=e.ask;if(void 0===d||u(d)||t.push(a(n.invalidAskSection,"`ask` 配置段不是对象,无法自动迁移,请手动修复。")),u(d))for(const e of Object.keys(o)){const r=o[e],i=g("router",s,r.routerKeys),u=g("ask",d,r.askKeys);i.ok?u.ok?i.key&&u.key&&u.value!==i.value&&t.push(a(n.routerAskConflict,`\`router.${r.targetAskKey}\` 与 \`ask.${r.targetAskKey}\` 同时存在且值冲突,请手动处理。`)):t.push(u.issue):t.push(i.issue)}return{matches:!0,canAutoMigrate:!t.some(e=>i.has(e.code)),issues:t}}function v(e){const t=y(e);if(!t.matches)return{ok:!0,changed:!1,document:structuredClone(e),issues:[],summary:[]};if(!t.canAutoMigrate)return{ok:!1,changed:!1,issues:t.issues};const s=structuredClone(e),n=s.router;if(!u(n))return{ok:!1,changed:!1,issues:t.issues};const r=s.ask,i=u(r)?r:{};let a=!1;const l=[];for(const e of Object.keys(o)){const t=o[e],s=g("router",n,t.routerKeys);if(!s.ok)return{ok:!1,changed:!1,issues:[s.issue]};if(!s.key)continue;const r=g("ask",i,t.askKeys);if(!r.ok)return{ok:!1,changed:!1,issues:[r.issue]};r.key?l.push(`移除已废弃的 \`router.${t.targetAskKey}\``):(i[t.targetAskKey]=s.value,a=!0,l.push(`将 \`router.${t.targetAskKey}\` 迁移到 \`ask.${t.targetAskKey}\``));for(const e of t.routerKeys)c(n,e)&&delete n[e]}return c(n,"mode")&&(delete n.mode,l.push("删除已废弃的 `router.mode`")),a&&(s.ask=i),0===Object.keys(n).length&&(delete s.router,l.push("删除空的 `router` 配置段")),{ok:!0,changed:l.length>0,document:s,issues:t.issues,summary:l}}const p=/^[a-z0-9]+(-[a-z0-9]+)*$/;function A(e){return!p.test(e)}function b(t){const s=e(t).toLowerCase();return p.test(s)?s:void 0}function K(e){const t=e.agents;if(!u(t))return{matches:!1,canAutoMigrate:!1,issues:[]};const s=t.env;if(!u(s))return{matches:!1,canAutoMigrate:!1,issues:[]};const o=Object.keys(s).filter(A);if(0===o.length)return{matches:!1,canAutoMigrate:!1,issues:[]};const r=[];let i=!1;for(const e of o){const t=b(e);void 0!==t?c(s,t)?(i=!0,r.push(a(n.legacyAgentEnvKeyConflict,`\`agents.env\` 下同时存在 \`${e}\` 与 \`${t}\`,无法自动合并,请手动处理。`))):r.push(a(n.legacyCamelCaseAgentEnvKey,`\`agents.env.${e}\` 应使用 kebab-case(\`${t}\`)。`)):(i=!0,r.push(a(n.legacyAgentEnvKeyConflict,`\`agents.env.${e}\` 命名不符合 kebab-case 规范,无法自动迁移,请手动重命名。`)))}return{matches:!0,canAutoMigrate:!i,issues:r}}function S(e){const t=K(e);if(!t.matches)return{ok:!0,changed:!1,document:structuredClone(e),issues:[],summary:[]};if(!t.canAutoMigrate)return{ok:!1,changed:!1,issues:t.issues};const s=structuredClone(e),n=s.agents;if(!u(n))return{ok:!1,changed:!1,issues:t.issues};const o=n.env;if(!u(o))return{ok:!1,changed:!1,issues:t.issues};const r=[];for(const e of Object.keys(o).filter(A)){const t=b(e);void 0!==t&&(o[t]=o[e],delete o[e],r.push(`将 \`agents.env.${e}\` 重命名为 \`agents.env.${t}\``))}return{ok:!0,changed:r.length>0,document:s,issues:t.issues,summary:r}}function M(e){const t=e.runtime;if(!u(t))return{matches:!1,canAutoMigrate:!1,issues:[]};if(!c(t,"bash"))return{matches:!1,canAutoMigrate:!1,issues:[]};const s=[a(n.deprecatedRuntimeBashSection,"`runtime.bash` 配置段已废弃,请改用 `runtime.shell`。")],o=t.bash;u(o)||s.push(a(n.invalidRuntimeBashSection,"`runtime.bash` 配置段不是对象,无法自动迁移,请手动修复。"));const r=t.shell;void 0===r||u(r)||s.push(a(n.invalidRuntimeShellSection,"`runtime.shell` 配置段不是对象,无法自动迁移,请手动修复。"));const d=u(o)?f(o,"runtime.bash"):void 0,h=u(r)?f(r,"runtime.shell"):void 0;for(const e of[d,h])void 0===e||e.ok||s.push(a(n.runtimeShellConflict,`\`${e.conflictPath}\` 同时包含等价键且值冲突,请手动处理。`));!0!==d?.ok||!0!==h?.ok||l(d.value,h.value)||s.push(a(n.runtimeShellConflict,"`runtime.bash` 与 `runtime.shell` 同时存在且值冲突,请手动处理。"));return{matches:!0,canAutoMigrate:!s.some(e=>i.has(e.code)),issues:s}}function C(e){const t=M(e);if(!t.matches)return{ok:!0,changed:!1,document:structuredClone(e),issues:[],summary:[]};if(!t.canAutoMigrate)return{ok:!1,changed:!1,issues:t.issues};const s=structuredClone(e),n=s.runtime;if(!u(n))return{ok:!1,changed:!1,issues:t.issues};const o=n.bash;if(!u(o))return{ok:!1,changed:!1,issues:t.issues};const r=[];return c(n,"shell")?r.push("删除已废弃的 `runtime.bash`"):(n.shell=m(o,d()),r.push("将 `runtime.bash` 迁移为 `runtime.shell`")),delete n.bash,{ok:!0,changed:!0,document:s,issues:t.issues,summary:r}}const j=[{id:"router-to-ask",scopes:new Set(["ask"]),inspect:y,apply:v},{id:"legacy-agent-env-keys",scopes:new Set(["agents"]),inspect:K,apply:S},{id:"runtime-bash-to-shell",scopes:new Set(["runtime"]),inspect:M,apply:C}];export function detectKnownConfigMigrations(e,t={}){const{scope:s}=t,n=(void 0!==s?j.filter(e=>e.scopes.has(s)):j).map(t=>t.inspect(e)).filter(e=>e.matches);return 0===n.length?{needsMigration:!1,canAutoMigrate:!1,issues:[]}:{needsMigration:!0,canAutoMigrate:n.every(e=>e.canAutoMigrate),issues:n.flatMap(e=>e.issues)}}export function applyKnownConfigMigrations(e){let t=structuredClone(e);const s=[],n=[];let o=!1;for(const e of j){const r=e.apply(t);if(!r.ok)return{ok:!1,changed:!1,issues:r.issues};t=r.document,r.changed&&(o=!0,s.push(...r.summary)),n.push(...r.issues)}return{ok:!0,changed:o,document:t,summary:s,issues:n}}export function formatConfigMigrationError(e,t){const s=[`Config validation failed (${e}):`];for(const e of t.issues)s.push(` - ${e.message}`);return t.canAutoMigrate?s.push(" - 可运行 `roll config migrate` 自动迁移当前配置。"):t.needsMigration&&s.push(" - 检测到配置迁移冲突,请手动处理后再重试。"),s.join("\n")}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static secret classification shared by config serialization and the UI catalog.
|
|
3
|
+
* Dynamic Agent env fields are layered on top by ConfigApplicationService.
|
|
4
|
+
*/
|
|
5
|
+
export declare function isRollConfigSecretPath(path: readonly (string | number)[]): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Classify unknown/future configuration names conservatively without substring matching.
|
|
8
|
+
*
|
|
9
|
+
* Only terminal words are considered so operational fields such as `tokenBudget`,
|
|
10
|
+
* `maxOutputTokens`, `secretRotation`, and `passwordPolicy` remain ordinary values.
|
|
11
|
+
*/
|
|
12
|
+
export declare function hasSecretTerminalName(path: readonly (string | number)[]): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Detect credentials embedded in an URL value. The check is intentionally value-aware:
|
|
15
|
+
* an ordinary endpoint stays visible, while user-info and credential query/fragment
|
|
16
|
+
* parameters cause the whole value to be protected by the keep-existing sentinel.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isCredentialBearingUrl(value: unknown): boolean;
|
|
19
|
+
/** Combine exact declarations, the terminal-name fallback, and URL-value inspection. */
|
|
20
|
+
export declare function isSecretConfigValue(path: readonly (string | number)[], value: unknown, isExactSecretPath?: (path: readonly (string | number)[]) => boolean): boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function isRollConfigSecretPath(e){return 4===e.length&&"llm"===e[0]&&"providers"===e[1]&&"apiKey"===e[3]||4===e.length&&"browser"===e[0]&&"instances"===e[1]&&"cdpUrl"===e[3]}export function hasSecretTerminalName(e){const r=e.at(-1);if("string"!=typeof r)return!1;const n=t(r),a=n.at(-1),i=n.at(-2);return"token"===a||"secret"===a||"password"===a||"webhook"===a||("key"===a&&("api"===i||"private"===i)||"url"===a&&"webhook"===i)}export function isCredentialBearingUrl(e){if("string"!=typeof e||0===e.length)return!1;if(/^[a-z][a-z\d+.-]*:\/\/[^/?#\s]+@/iu.test(e))return!0;try{const t=new URL(e);return t.username.length>0||t.password.length>0||(!!r(t.searchParams)||r(new URLSearchParams(n(t.hash))))}catch{return!!/^[a-z][a-z\d+.-]*:\/\//iu.test(e)&&(r(a(e,"?"))||r(a(e,"#")))}}export function isSecretConfigValue(t,r,n=isRollConfigSecretPath){return n(t)||!e(t)&&hasSecretTerminalName(t)||isCredentialBearingUrl(r)}function e(e){return e.length>=4&&"agents"===e[0]&&"env"===e[1]}function t(e){return e.replace(/([a-z\d])([A-Z])/gu,"$1 $2").replace(/([A-Z]+)([A-Z][a-z])/gu,"$1 $2").split(/[^A-Za-z\d]+/u).filter(e=>e.length>0).map(e=>e.toLowerCase())}function r(e){for(const r of e.keys()){const e=t(r),n=e.at(-1),a=e.at(-2);if("token"===n||"secret"===n||"password"===n||"credential"===n||"signature"===n||"sig"===n||"auth"===n||"apikey"===n||"privatekey"===n||"key"===n&&("api"===a||"private"===a))return!0}return!1}function n(e){return e.startsWith("#")?e.slice(1):e}function a(e,t){const r=e.indexOf(t);if(-1===r)return new URLSearchParams;const n=e.slice(r+1).split("?"===t?"#":"?",1)[0]??"";return new URLSearchParams(n)}
|
package/dist/llm/providers.d.ts
CHANGED
|
@@ -32,5 +32,7 @@ export declare function toSamplingConnectOptions(call: ResolvedLLMCall | undefin
|
|
|
32
32
|
*
|
|
33
33
|
* chat 与 sampling 场景下都是纯文本 generateText 调用,复用同一套
|
|
34
34
|
* thinkingProviderOptions 映射注入 reasoning/thinking effort。
|
|
35
|
+
* 显式 baseURL 的 OpenAI Responses 调用使用无服务端状态的历史重放,
|
|
36
|
+
* 避免兼容端点返回 item ID 却未持久化 item 时导致后续轮次失败。
|
|
35
37
|
*/
|
|
36
38
|
export declare function resolveLLMCall(providerName: string, modelName: string, apiKey: string, purpose: LLMCallPurpose, baseURL?: string, thinkingLevel?: ThinkingLevel): ResolvedLLMCall;
|
package/dist/llm/providers.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createAlibaba as e}from"@ai-sdk/alibaba";import{createAnthropic as i}from"@ai-sdk/anthropic";import{createOpenAI as
|
|
1
|
+
import{createAlibaba as e}from"@ai-sdk/alibaba";import{createAnthropic as i}from"@ai-sdk/anthropic";import{createOpenAI as o}from"@ai-sdk/openai";import{createDeepSeek as n}from"@ai-sdk/deepseek";import{runtimeThinkingLevels as t}from"../config/schema.js";const r={low:2048,medium:8192,high:16384},a=["gpt-5.1","gpt-5.2","gpt-5.3","gpt-5.4"];function p(e){return a.some(i=>e.startsWith(i))}function s(e){const i=/^claude-[a-z]+-(\d+)(?:-(\d+))?(?:\b|-)/.exec(e),o=i?.[1];if(!o)return!1;const n=Number(o),t=i?.[2],r=void 0!==t&&t.length<=2?Number(t):0;return n>4||4===n&&r>=6}export function thinkingProviderOptions(e,i,o){return"openai"===e?"off"===o?p(i)?{openai:{reasoningEffort:"none"}}:void 0:{openai:{reasoningEffort:o}}:"anthropic"===e?"off"===o?{anthropic:{thinking:{type:"disabled"}}}:s(i)?{anthropic:{thinking:{type:"adaptive"},effort:o}}:{anthropic:{thinking:{type:"enabled",budgetTokens:r[o]}}}:"qwen"===e?"off"===o?{alibaba:{enableThinking:!1}}:{alibaba:{enableThinking:!0,thinkingBudget:r[o]}}:"deepseek"===e?{deepseek:{thinking:{type:"off"===o?"disabled":"enabled"}}}:void 0}const d="https://dashscope.aliyuncs.com/compatible-mode/v1",c={anthropic:(e,{apiKey:o,baseURL:n})=>i({apiKey:o,...n?{baseURL:n}:{}})(e),openai:(e,{apiKey:i,baseURL:n})=>o({apiKey:i,...n?{baseURL:n}:{}})(e),deepseek:(e,{apiKey:i,baseURL:o})=>n({apiKey:i,...o?{baseURL:o}:{}})(e),qwen:(i,{apiKey:o,baseURL:n})=>e({apiKey:o,baseURL:n??d})(i)};export function createProviderModel(e,i,o,n){const t=c[e];if(!t){const i=Object.keys(c).join(", ");throw new Error(`Unknown LLM provider "${e}". Supported: ${i}`)}return t(i,{apiKey:o,baseURL:n})}export function toSamplingConnectOptions(e){return e?{samplingModel:e.model,...e.providerOptions?{samplingProviderOptions:e.providerOptions}:{}}:{}}export function resolveLLMCall(e,i,o,n,t,r="medium"){const a=createProviderModel(e,i,o,t);if("structured-output"===n&&"qwen"===e)return{model:a,providerOptions:{alibaba:{enableThinking:!1}}};if("chat"===n||"sampling"===n){const o=thinkingProviderOptions(e,i,r);return"openai"===e&&t?{model:a,providerOptions:{...o??{},openai:{...o?.openai??{},store:!1}}}:o?{model:a,providerOptions:o}:{model:a}}return{model:a}}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ConfigActivationEffect } from "../config/application-service.ts";
|
|
2
|
+
import type { RollConfig } from "../config/schema.ts";
|
|
3
|
+
import type { AgentRuntimeOwnership, AgentStatus, RegisteredAgent } from "../types/agent.ts";
|
|
4
|
+
import { type AgentLifecycleLock, type ManagedAgentRuntimeIdentity, type ManagedAgentRuntimeInspection } from "./process-manager.ts";
|
|
5
|
+
export declare const AGENT_LIFECYCLE_STATES: readonly ["ready-on-demand", "running", "stopped", "unreachable", "external-online", "external-unreachable"];
|
|
6
|
+
export type AgentLifecycleState = (typeof AGENT_LIFECYCLE_STATES)[number];
|
|
7
|
+
export declare const AGENT_ACTIVATION_STATUSES: readonly ["deferred", "restarted", "kept-stopped", "next-invocation", "manual", "runtime-changed", "failed"];
|
|
8
|
+
export type AgentActivationStatus = (typeof AGENT_ACTIVATION_STATUSES)[number];
|
|
9
|
+
export interface BrowserRuntimeInspectionBoundary {
|
|
10
|
+
readonly state: "not-inspected";
|
|
11
|
+
readonly message: string;
|
|
12
|
+
}
|
|
13
|
+
export interface AgentLifecycleInspection {
|
|
14
|
+
readonly agentName: string;
|
|
15
|
+
readonly ownership: AgentRuntimeOwnership;
|
|
16
|
+
readonly transport: RegisteredAgent["transport"]["type"];
|
|
17
|
+
readonly state: AgentLifecycleState;
|
|
18
|
+
readonly endpointReachable: boolean | null;
|
|
19
|
+
readonly canAutoRestart: boolean;
|
|
20
|
+
readonly pid?: number;
|
|
21
|
+
readonly endpoint?: string;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
readonly browserRuntime?: BrowserRuntimeInspectionBoundary;
|
|
24
|
+
}
|
|
25
|
+
export interface AgentLifecycleBaselineState {
|
|
26
|
+
readonly agent: RegisteredAgent;
|
|
27
|
+
readonly pid?: number;
|
|
28
|
+
readonly runtimeIdentity?: AgentLifecycleRuntimeIdentity;
|
|
29
|
+
}
|
|
30
|
+
export type AgentLifecycleRuntimeIdentity = ManagedAgentRuntimeIdentity;
|
|
31
|
+
/**
|
|
32
|
+
* 保存配置前捕获的常驻状态。配置应用只会重启这里记录为运行中的 core-managed Agent。
|
|
33
|
+
*/
|
|
34
|
+
export interface AgentLifecycleBaseline {
|
|
35
|
+
readonly dataDir: string;
|
|
36
|
+
readonly capturedAt: string;
|
|
37
|
+
readonly agents: Readonly<Record<string, AgentLifecycleBaselineState>>;
|
|
38
|
+
}
|
|
39
|
+
export interface AgentActivationResultItem {
|
|
40
|
+
readonly effect: ConfigActivationEffect;
|
|
41
|
+
readonly status: AgentActivationStatus;
|
|
42
|
+
readonly message: string;
|
|
43
|
+
readonly pid?: number;
|
|
44
|
+
}
|
|
45
|
+
export interface AgentActivationResult {
|
|
46
|
+
readonly success: boolean;
|
|
47
|
+
readonly requiresManualAction: boolean;
|
|
48
|
+
readonly restartedAgentNames: readonly string[];
|
|
49
|
+
readonly items: readonly AgentActivationResultItem[];
|
|
50
|
+
}
|
|
51
|
+
export interface AgentLifecycleCollaborators {
|
|
52
|
+
readonly readAgents: (dataDir: string) => ReadonlyArray<RegisteredAgent>;
|
|
53
|
+
readonly updateStatus: (dataDir: string, agentName: string, status: AgentStatus) => void;
|
|
54
|
+
readonly getPid: (dataDir: string, agentName: string) => number | undefined;
|
|
55
|
+
readonly inspectRuntime: (agent: RegisteredAgent, dataDir: string) => ManagedAgentRuntimeInspection;
|
|
56
|
+
readonly probe: (agent: RegisteredAgent, options?: {
|
|
57
|
+
readonly timeoutMs?: number;
|
|
58
|
+
}) => Promise<void>;
|
|
59
|
+
readonly start: (agent: RegisteredAgent, dataDir: string, env?: Readonly<Record<string, string>>, options?: {
|
|
60
|
+
readonly lifecycleLock?: AgentLifecycleLock;
|
|
61
|
+
}) => number;
|
|
62
|
+
readonly stopGracefully: (dataDir: string, agentName: string, options?: {
|
|
63
|
+
readonly timeoutMs?: number;
|
|
64
|
+
readonly intervalMs?: number;
|
|
65
|
+
readonly expectedIdentity?: AgentLifecycleRuntimeIdentity;
|
|
66
|
+
readonly lifecycleLock?: AgentLifecycleLock;
|
|
67
|
+
}) => Promise<boolean>;
|
|
68
|
+
readonly waitUntilReady: (agent: RegisteredAgent, options?: {
|
|
69
|
+
readonly startupTimeoutMs?: number;
|
|
70
|
+
readonly probeTimeoutMs?: number;
|
|
71
|
+
readonly intervalMs?: number;
|
|
72
|
+
}) => Promise<void>;
|
|
73
|
+
readonly resolveAgentEnv: (config: RollConfig, agentName: string) => Readonly<Record<string, string>> | undefined;
|
|
74
|
+
readonly acquireLifecycleLock: (dataDir: string, agentName: string) => AgentLifecycleLock;
|
|
75
|
+
}
|
|
76
|
+
export interface AgentInspectionOptions {
|
|
77
|
+
readonly probeTimeoutMs?: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* CLI 与本地 UI 可共同复用的 Agent 状态检查和配置生效服务。
|
|
81
|
+
*
|
|
82
|
+
* 此服务只管理 Agent 进程/MCP endpoint;即使 browser-use-agent 显示 running,
|
|
83
|
+
* 也不代表 Chrome 已启动,浏览器实例仍由首次浏览器工具调用懒启动。
|
|
84
|
+
*/
|
|
85
|
+
export declare class AgentLifecycleService {
|
|
86
|
+
readonly dataDir: string;
|
|
87
|
+
private readonly collaborators;
|
|
88
|
+
constructor(dataDir: string, collaborators?: Partial<AgentLifecycleCollaborators>);
|
|
89
|
+
inspectAll(options?: AgentInspectionOptions): Promise<readonly AgentLifecycleInspection[]>;
|
|
90
|
+
captureBaseline(): AgentLifecycleBaseline;
|
|
91
|
+
/** 显式重启一个当前正在运行的 core-managed Agent;不会把 stopped Agent 启动起来。 */
|
|
92
|
+
restartRunningAgent(agentName: string, effectiveConfig: RollConfig): Promise<AgentActivationResultItem>;
|
|
93
|
+
applyActivation(effects: readonly ConfigActivationEffect[], baseline: AgentLifecycleBaseline, effectiveConfig: RollConfig): Promise<AgentActivationResult>;
|
|
94
|
+
private inspectAgent;
|
|
95
|
+
private applyRestartEffect;
|
|
96
|
+
private restartCoreManagedAgent;
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{getAgentEnv as t}from"../config/helpers.js";import{acquireAgentLifecycleLock as e,getAgentPid as a,inspectManagedAgentRuntime as n,probeAgentEndpoint as r,startAgent as i,stopAgentGracefully as s,waitForAgentReady as o}from"./process-manager.js";import{AgentStore as c}from"./store.js";const d="browser-use-agent";export const AGENT_LIFECYCLE_STATES=["ready-on-demand","running","stopped","unreachable","external-online","external-unreachable"];export const AGENT_ACTIVATION_STATUSES=["deferred","restarted","kept-stopped","next-invocation","manual","runtime-changed","failed"];const l={readAgents:t=>new c(t).list(),updateStatus:(t,e,a)=>{new c(t).updateStatus(e,a)},getPid:a,inspectRuntime:n,probe:r,start:i,stopGracefully:s,waitUntilReady:o,resolveAgentEnv:t,acquireLifecycleLock:e};export class AgentLifecycleService{dataDir;collaborators;constructor(t,e={}){this.dataDir=t,this.collaborators={...l,...e}}async inspectAll(t={}){const e=this.collaborators.readAgents(this.dataDir);return Promise.all(e.map(e=>this.inspectAgent(e,t)))}captureBaseline(){const t={};for(const e of this.collaborators.readAgents(this.dataDir)){const a="core-managed"===e.runtime.ownership?this.collaborators.inspectRuntime(e,this.dataDir):void 0,n=a?.pid,r=void 0===a?void 0:u(e,a);t[e.skill.name]={agent:e,...void 0!==n?{pid:n}:{},...void 0!==r?{runtimeIdentity:r}:{}}}return{dataDir:this.dataDir,capturedAt:(new Date).toISOString(),agents:t}}async restartRunningAgent(t,e){const a={kind:"restart-agent",paths:[],title:`重启 ${t}`,description:"重启当前运行中的 core-managed Agent。",agentName:t,requiresConfirmation:!0},n=this.captureBaseline();return this.applyRestartEffect(a,n,e,!1)}async applyActivation(t,e,a){if(e.dataDir!==this.dataDir)throw new Error(`Agent lifecycle baseline dataDir mismatch: expected ${this.dataDir}, received ${e.dataDir}`);const n=t.some(t=>"manual"===t.kind&&t.paths.some(y)),r=new Set,i=[];for(const s of t)if("restart-agent"!==s.kind)i.push(k(s));else{if(void 0!==s.agentName){if(r.has(s.agentName))continue;r.add(s.agentName)}i.push(await this.applyRestartEffect(s,e,a,n))}const s=new Set(["manual","runtime-changed","failed"]);return{success:i.every(t=>"failed"!==t.status),requiresManualAction:i.some(t=>s.has(t.status)),restartedAgentNames:i.flatMap(t=>"restarted"===t.status&&void 0!==t.effect.agentName?[t.effect.agentName]:[]),items:i}}async inspectAgent(t,e){const a=m(t);switch(t.runtime.ownership){case"on-demand":return{...a,state:"ready-on-demand",endpointReachable:null,canAutoRestart:!1,message:"按需模式:配置会在下一次 run/ask 调用时生效,无常驻进程可重启。"};case"external-managed":try{return await this.collaborators.probe(t,g(e)),{...a,state:"external-online",endpointReachable:!0,canAutoRestart:!1,message:t.skill.name===d?"browser-use-agent 的外部 MCP endpoint 可连接;这不代表 Chrome 已启动。":"外部托管 endpoint 可连接;启动、停止和重启仍由外部系统负责。",...f(t)}}catch{return{...a,state:"external-unreachable",endpointReachable:!1,canAutoRestart:!1,message:"外部托管 endpoint 不可连接;请检查外部服务状态和 Agent 日志。",...f(t)}}case"core-managed":{const n=this.collaborators.inspectRuntime(t,this.dataDir),r=n.pid;if(void 0===r)return{...a,state:"stopped",endpointReachable:null,canAutoRestart:!1,message:"Agent 当前已停止;保存配置不会把它自动启动。"};if(void 0===u(t,n))return{...a,state:"unreachable",endpointReachable:null,canAutoRestart:!1,pid:r,message:"Agent PID 存在,但 runtime 身份无法安全验证;已禁用自动停止/重启,请按诊断提示人工处理。",...f(t)};try{return await this.collaborators.probe(t,g(e)),{...a,state:"running",endpointReachable:!0,canAutoRestart:!0,pid:r,message:b(t),...f(t)}}catch{return{...a,state:"unreachable",endpointReachable:!1,canAutoRestart:!0,pid:r,message:"Agent 进程存在,但 MCP endpoint 不可连接;请检查 Agent 日志。",...f(t)}}}}}async applyRestartEffect(t,e,a,n){const r=t.agentName;if(void 0===r)return v(t,"manual","生效计划没有提供 Agent 名称,无法自动重启。");if(n)return v(t,"manual","`agents.dataDir` 已变更;先停止 Agent 并人工迁移 PID、日志和注册数据,再执行重启。");const i=e.agents[r];if(void 0===i)return v(t,"manual",`保存前的 Agent 列表中没有 ${r},不会自动启动。`);switch(i.agent.runtime.ownership){case"on-demand":return v(t,"next-invocation",`${r} 为按需模式,配置会在下一次 run/ask 调用时生效。`);case"external-managed":return v(t,"manual",`${r} 由外部系统管理,请在外部完成重启。`)}return void 0===i.pid?v(t,"kept-stopped",`${r} 保存前已停止,保持停止;下次手动启动时会读取新配置。`):void 0===i.runtimeIdentity?v(t,"runtime-changed",`${r} 保存前的进程缺少可验证的 runtime sidecar;为避免停止无关进程,未自动应用。`):this.restartCoreManagedAgent(t,i.agent,i.runtimeIdentity,a)}async restartCoreManagedAgent(t,e,a,n){const r=a.pid;let i,s,o;try{o=this.collaborators.acquireLifecycleLock(this.dataDir,e.skill.name);const c=this.collaborators.inspectRuntime(e,this.dataDir);if(!p(u(e,c),a)){const a=c.pid;return v(t,"runtime-changed",void 0===a?`${e.skill.name} 在保存后已被停止,为避免意外重新启动,未自动应用。`:a!==r?`${e.skill.name} 的 PID 已从 ${String(r)} 变为 ${String(a)},为避免停止新进程,未自动应用。`:`${e.skill.name} 的 runtime sidecar 已缺失、失效或不再对应保存前的进程;为避免停止无关进程,未自动应用。`)}if(!await this.collaborators.stopGracefully(this.dataDir,e.skill.name,{expectedIdentity:a,lifecycleLock:o}))return v(t,"runtime-changed",`${e.skill.name} 在应用前已不再运行,为避免意外启动,未自动应用。`);this.collaborators.updateStatus(this.dataDir,e.skill.name,"starting");const d=this.collaborators.resolveAgentEnv(n,e.skill.name);if(i=this.collaborators.start(e,this.dataDir,d,{lifecycleLock:o}),s=u(e,this.collaborators.inspectRuntime(e,this.dataDir)),s?.pid!==i)throw new Error("Started Agent runtime identity could not be verified.");return await this.collaborators.waitUntilReady(e),this.collaborators.updateStatus(this.dataDir,e.skill.name,"online"),v(t,"restarted",A(e),i)}catch{let a=!1;if(void 0!==i){const t=this.collaborators.inspectRuntime(e,this.dataDir),n=u(e,t);let r=t.pid;void 0!==s&&p(n,s)&&(await this.collaborators.stopGracefully(this.dataDir,e.skill.name,{expectedIdentity:s,...void 0!==o?{lifecycleLock:o}:{}}).catch(()=>!1),r=this.collaborators.getPid(this.dataDir,e.skill.name)),a=void 0!==r&&r!==i}return void 0===o||a||this.collaborators.updateStatus(this.dataDir,e.skill.name,"error"),v(t,"failed",`${e.skill.name} 重启失败;请检查 Agent 日志。`)}finally{o?.release()}}}function u(t,e){const{pid:a,sidecar:n}=e;if(!(void 0===a||void 0===n||e.issues.length>0||n.agentName!==t.skill.name||n.pid!==a||n.coreVersion!==e.expectedCoreVersion)){if("streamable-http"===t.transport.type){if(e.expectedEndpoint!==t.transport.endpoint||n.endpoint!==t.transport.endpoint)return}else if(void 0!==n.endpoint||void 0!==e.expectedEndpoint)return;return{pid:a,processStartToken:n.processStartToken,startedAt:n.startedAt}}}function p(t,e){return t?.pid===e.pid&&t.processStartToken===e.processStartToken&&t.startedAt===e.startedAt}function m(t){return{agentName:t.skill.name,ownership:t.runtime.ownership,transport:t.transport.type,..."streamable-http"===t.transport.type?{endpoint:h(t.transport.endpoint)}:{}}}function h(t){try{const e=new URL(t);return e.username="",e.password="",e.search="",e.hash="",e.toString()}catch{return"[invalid endpoint]"}}function g(t){return void 0!==t.probeTimeoutMs?{timeoutMs:t.probeTimeoutMs}:{}}function f(t){return t.skill.name!==d?{}:{browserRuntime:{state:"not-inspected",message:"这里只检查 browser-use-agent 的 MCP endpoint;不代表 Chrome 已启动,Chrome 仍在首次浏览器工具调用时懒启动。"}}}function b(t){return t.skill.name===d?"browser-use-agent 的 MCP endpoint 已在线;这不代表 Chrome 已启动。":"Agent 进程与 MCP endpoint 均在线。"}function A(t){return t.skill.name===d?"browser-use-agent 的 MCP endpoint 已重启并在线;Chrome 仍会在首次浏览器工具调用时懒启动。":`${t.skill.name} 已重启并在线。`}function y(t){return"agents"===t[0]&&"dataDir"===t[1]}function k(t){switch(t.kind){case"next-command":return v(t,"deferred","配置已保存,后续 Roll 命令会重新加载。 ");case"next-chat":return v(t,"deferred","配置已保存,新建 roll chat 会话后生效。 ");case"manual":return v(t,"manual",t.description);case"restart-agent":return v(t,"manual","未执行 Agent 重启。 ")}}function v(t,e,a,n){return{effect:t,status:e,message:a.trim(),...void 0!==n?{pid:n}:{}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{readFileSync as t,existsSync as r,realpathSync as
|
|
1
|
+
import{readFileSync as t,existsSync as r,realpathSync as e}from"node:fs";import{readJsonFile as n}from"./json-file.js";import{resolve as o,sep as i}from"node:path";import a from"gray-matter";import{z as s}from"zod";import{parse as p}from"yaml";import{AGENT_ENV_VALUE_TYPES as l,createDefaultRuntimeForTransport as d}from"../types/agent.js";const c="SKILL.md",m="package.json",u=s.object({name:s.string().min(1),purpose:s.string().optional(),example:s.string().optional(),default:s.string().optional(),type:s.enum(l).optional(),secret:s.boolean().optional(),configurable:s.boolean().optional(),sourcePath:s.array(s.string().min(1)).min(1).optional()}),f=s.object({required:s.array(u).optional(),optional:s.array(u).optional()}),g=s.object({required:s.boolean().optional(),purpose:s.string().optional(),example:s.string().optional(),default:s.string().optional(),type:s.enum(l).optional(),secret:s.boolean().optional(),configurable:s.boolean().optional(),sourcePath:s.array(s.string().min(1)).min(1).optional()}),h=s.object({env:s.record(g).optional()});export function discoverAgent(e){const n=o(e),i=o(n,c);if(!r(i))throw new Error(`SKILL.md not found in ${n}`);const s=t(i,"utf-8"),{data:p,content:l}=a(s),m=E(n),u=p,f=u.name,g=u.description;if("string"!=typeof f||0===f.length)throw new Error(`SKILL.md missing required field "name" in ${i}`);if("string"!=typeof g||0===g.length)throw new Error(`SKILL.md missing required field "description" in ${i}`);const h=u.metadata??{},y={};for(const[t,r]of Object.entries(h))y[t]=String(r);const w="string"==typeof u.license?u.license:void 0,b="string"==typeof u.compatibility?u.compatibility:void 0,$=v(n,y,i),L={name:f,description:g,...w?{license:w}:{},...b?{compatibility:b}:{},metadata:y,...$?{env:$}:{}},I=m?S(m):void 0;if(I&&x(y)){if(!K(j(y),I.transport))throw new Error(`Conflicting runtime metadata in ${i}: package.json#rollAgent and SKILL.md metadata disagree`)}const q=I?.transport??j(y);return{skill:L,transport:q,runtime:I?.runtime??d(q),skillPath:i,skillBody:l.trim()}}function y(t,r){if(void 0===t)return;if(P(t)&&"env"in t)return w(t,r);const e=f.safeParse(t);if(!e.success){const t=e.error.issues.map(t=>`${t.path.length>0?`env.${t.path.join(".")}`:"env"}: ${t.message}`).join("; ");throw new Error(`SKILL.md has invalid "env" declaration in ${r}: ${t}`)}const n=e.data;return n.required||n.optional?{...n.required?{required:n.required.map($)}:{},...n.optional?{optional:n.optional.map($)}:{}}:void 0}function w(t,r){const e=h.safeParse(t);if(!e.success){const t=e.error.issues.map(t=>`${t.path.length>0?t.path.join("."):"env"}: ${t.message}`).join("; ");throw new Error(`SKILL.md has invalid "env" declaration in ${r}: ${t}`)}const n=e.data.env;if(!n)return;const o=[],i=[];for(const[t,e]of Object.entries(n)){if(0===t.length)throw new Error(`SKILL.md has invalid "env" declaration in ${r}: env key is empty`);const n=L(t,e);!0===e.required?o.push(n):i.push(n)}return 0!==o.length||0!==i.length?{...o.length>0?{required:o}:{},...i.length>0?{optional:i}:{}}:void 0}function v(n,i,a){const s=i["roll-env-file"];if(!s)return;const l=o(n,s);if(!b(n,l))throw new Error(`SKILL.md roll-env-file must stay within agent directory: ${a}`);if(!r(l))throw new Error(`SKILL.md roll-env-file not found: ${l}`);const d=e(n),c=e(l);if(!b(d,c))throw new Error(`SKILL.md roll-env-file must stay within agent directory: ${a}`);let m;try{m=p(t(c,"utf-8"))}catch(t){throw new Error(`Failed to parse roll-env-file for ${a}: ${t instanceof Error?t.message:String(t)}`)}return y(m,c)}function b(t,r){return r===t||r.startsWith(`${t}${i}`)}function $(t){return{name:t.name,...t.purpose?{purpose:t.purpose}:{},...t.example?{example:t.example}:{},...t.default?{default:t.default}:{},...void 0!==t.type?{type:t.type}:{},...void 0!==t.secret?{secret:t.secret}:{},...void 0!==t.configurable?{configurable:t.configurable}:{},...void 0!==t.sourcePath?{sourcePath:t.sourcePath}:{}}}function L(t,r){return{name:t,...r.purpose?{purpose:r.purpose}:{},...r.example?{example:r.example}:{},...r.default?{default:r.default}:{},...void 0!==r.type?{type:r.type}:{},...void 0!==r.secret?{secret:r.secret}:{},...void 0!==r.configurable?{configurable:r.configurable}:{},...void 0!==r.sourcePath?{sourcePath:r.sourcePath}:{}}}function j(t){if("streamable-http"===("streamable-http"===t["roll-transport"]?"streamable-http":"stdio")){const r=t["roll-endpoint"];if(!r)throw new Error('SKILL.md declares streamable-http transport but missing "roll-endpoint"');return{type:"streamable-http",endpoint:r}}const r=(t["roll-command"]??"node --experimental-strip-types src/index.ts").split(/\s+/),e=r[0],n=r.slice(1);if(!e)throw new Error("Invalid roll-command in SKILL.md");return n.length>0?{type:"stdio",command:e,args:n}:{type:"stdio",command:e}}function E(t){const e=o(t,m);if(!r(e))return;let i;try{i=n(e)}catch{throw new Error(`Invalid package.json in ${t}`)}if(!P(i)||!P(i.rollAgent))return;const a=i.rollAgent,s=P(a.runtime)?a.runtime:void 0;if(!s)throw new Error(`package.json#rollAgent.runtime is required in ${e}`);const p={runtime:{ownership:k(s.ownership)??"",transport:k(s.transport)??""}};if(P(a.start)){const t=k(a.start.command),r=A(a.start.args);(t||r)&&(p.start={...t?{command:t}:{},...r?{args:r}:{}})}if(P(a.endpoint)){const t=k(a.endpoint.url),r=k(a.endpoint.path),e="number"==typeof a.endpoint.port?a.endpoint.port:void 0;(t||r||void 0!==e)&&(p.endpoint={...t?{url:t}:{},...r?{path:r}:{},...void 0!==e?{port:e}:{}})}if(P(a.setup)&&P(a.setup.playwright)){const t=A(a.setup.playwright.browsers);t&&t.length>0&&(p.setup={playwright:{browsers:t}})}return p}function S(t){const{ownership:r,transport:e}=t.runtime;if("on-demand"===r&&"stdio"===e){const r=t.start?.command;if(!r)throw new Error("package.json#rollAgent.start.command is required for stdio runtime");const e=t.start?.args;return{transport:e?{type:"stdio",command:r,args:e}:{type:"stdio",command:r},runtime:{ownership:"on-demand"}}}if("core-managed"===r&&"streamable-http"===e){const r=t.start?.command,e=I(t),n=t.endpoint?.path,o=t.endpoint?.port;if(!r||!n||void 0===o)throw new Error("package.json#rollAgent for core-managed streamable-http requires start.command, endpoint.path and endpoint.port");const i=t.start?.args,a=t.setup?.playwright?.browsers;return{transport:{type:"streamable-http",endpoint:e},runtime:{ownership:"core-managed",start:i?{command:r,args:i}:{command:r},endpoint:{path:n,port:o},...a&&a.length>0?{setup:{playwright:{browsers:a}}}:{}}}}if("external-managed"===r&&"streamable-http"===e)return{transport:{type:"streamable-http",endpoint:I(t)},runtime:{ownership:"external-managed"}};throw new Error(`Unsupported package.json#rollAgent runtime combination: ${r}/${e}`)}function I(t){const r=t.endpoint?.url;if(r)return r;const e=t.endpoint?.path,n=t.endpoint?.port;if(!e||void 0===n)throw new Error("package.json#rollAgent.streamable-http requires endpoint.url or endpoint.path + endpoint.port");return q(e,n)}function q(t,r){const e=t.startsWith("/")?t:`/${t}`;return`http://127.0.0.1:${String(r)}${e}`}function x(t){return"string"==typeof t["roll-transport"]||"string"==typeof t["roll-endpoint"]||"string"==typeof t["roll-command"]}function K(t,r){if(t.type!==r.type)return!1;if("streamable-http"===t.type&&"streamable-http"===r.type)return t.endpoint===r.endpoint;if("stdio"!==t.type||"stdio"!==r.type)return!1;const e=t.args??[],n=r.args??[];return t.command===r.command&&e.join("\0")===n.join("\0")}function k(t){return"string"==typeof t&&t.length>0?t:void 0}function A(t){if(!Array.isArray(t))return;const r=t.filter(t=>"string"==typeof t);return r.length>0?r:void 0}function P(t){return"object"==typeof t&&null!==t}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
declare const PROCESS_START_TOKEN_BRAND: unique symbol;
|
|
2
|
+
/** Opaque digest of an OS-reported process creation identity. */
|
|
3
|
+
export type ProcessStartToken = string & {
|
|
4
|
+
readonly [PROCESS_START_TOKEN_BRAND]: true;
|
|
5
|
+
};
|
|
6
|
+
export declare function isProcessStartToken(value: unknown): value is ProcessStartToken;
|
|
7
|
+
/**
|
|
8
|
+
* Reads an OS-owned process creation value and turns it into a stable opaque token.
|
|
9
|
+
*
|
|
10
|
+
* Returning `undefined` is intentional: callers must fail closed instead of treating a PID as
|
|
11
|
+
* identity when the platform cannot prove which process instance currently owns it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function readProcessStartToken(pid: number): ProcessStartToken | undefined;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{createHash as t}from"node:crypto";import{spawnSync as r}from"node:child_process";import{readFileSync as n}from"node:fs";const o=/^pst-v1:[a-f0-9]{64}$/u,e=2e3;export function isProcessStartToken(t){return"string"==typeof t&&o.test(t)}export function readProcessStartToken(r){if(!Number.isInteger(r)||r<=0)return;const n=s(r);if(void 0===n)return;const o=`pst-v1:${t("sha256").update(n).digest("hex")}`;return isProcessStartToken(o)?o:void 0}function s(t){switch(process.platform){case"linux":case"android":return i(t);case"darwin":return c(t);case"win32":return u(t);default:return d(t)}}function i(t){try{const r=n(`/proc/${String(t)}/stat`,"utf-8"),o=r.lastIndexOf(")");if(o<0)return;const e=r.slice(o+1).trim().split(/\s+/u)[19];if(void 0===e||!/^\d+$/u.test(e))return;const s=n("/proc/sys/kernel/random/boot_id","utf-8").trim();if(0===s.length)return;return`linux:${s}:${e}`}catch{return}}function c(t){const r=a("/bin/ps",["-p",String(t),"-o","lstart="]),n=a("/usr/sbin/sysctl",["-n","kern.boottime"]);if(void 0!==r&&void 0!==n)return`darwin:${n}:${r}`}function u(t){const r=["-NoLogo","-NoProfile","-NonInteractive","-Command",`$p = Get-Process -Id ${String(t)} -ErrorAction Stop; $p.StartTime.ToUniversalTime().Ticks`],n=a("powershell.exe",r)??a("pwsh.exe",r);if(void 0!==n&&/^\d+$/u.test(n))return`win32:${n}`}function d(t){const r=a("ps",["-p",String(t),"-o","lstart="]);return void 0===r?void 0:`${process.platform}:${r}`}function a(t,n){try{const o=r(t,[...n],{encoding:"utf-8",env:{...process.env,LC_ALL:"C",LANG:"C"},shell:!1,timeout:e,windowsHide:!0}),s=o.stdout.trim();return 0===o.status&&void 0===o.error&&s.length>0?s:void 0}catch{return}}
|
|
@@ -1,12 +1,28 @@
|
|
|
1
|
+
import { type ProcessStartToken } from "./process-identity.ts";
|
|
1
2
|
import type { RegisteredAgent } from "../types/agent.ts";
|
|
2
|
-
declare const RUNTIME_SIDECAR_SCHEMA_VERSION:
|
|
3
|
-
export
|
|
4
|
-
|
|
3
|
+
declare const RUNTIME_SIDECAR_SCHEMA_VERSION: 2;
|
|
4
|
+
export interface AgentLifecycleLock {
|
|
5
|
+
release(): void;
|
|
6
|
+
}
|
|
7
|
+
export declare class AgentLifecycleBusyError extends Error {
|
|
8
|
+
readonly code: "agent_lifecycle_busy";
|
|
9
|
+
constructor(agentName: string);
|
|
10
|
+
}
|
|
11
|
+
export declare class AgentRuntimeIdentityError extends Error {
|
|
12
|
+
readonly code: "agent_runtime_identity_unverifiable";
|
|
13
|
+
readonly pid: number;
|
|
14
|
+
constructor(agentName: string, pid: number, reason: string);
|
|
15
|
+
}
|
|
16
|
+
export type ManagedAgentRuntimeIssueCode = "missing-sidecar" | "invalid-sidecar" | "orphan-sidecar" | "agent-name-mismatch" | "pid-mismatch" | "process-identity-unavailable" | "process-identity-mismatch" | "version-mismatch" | "endpoint-mismatch";
|
|
17
|
+
export interface ManagedAgentRuntimeIdentity {
|
|
18
|
+
readonly pid: number;
|
|
19
|
+
readonly processStartToken: ProcessStartToken;
|
|
20
|
+
readonly startedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ManagedAgentRuntimeSidecar extends ManagedAgentRuntimeIdentity {
|
|
5
23
|
readonly schemaVersion: typeof RUNTIME_SIDECAR_SCHEMA_VERSION;
|
|
6
24
|
readonly agentName: string;
|
|
7
|
-
readonly pid: number;
|
|
8
25
|
readonly coreVersion: string;
|
|
9
|
-
readonly startedAt: string;
|
|
10
26
|
readonly endpoint?: string;
|
|
11
27
|
}
|
|
12
28
|
export interface ManagedAgentRuntimeIssue {
|
|
@@ -25,7 +41,7 @@ export interface ManagedAgentRuntimeInspection {
|
|
|
25
41
|
export declare function cleanupOrphanAgentRuntimeMetadata(dataDir: string, agentName: string): boolean;
|
|
26
42
|
/** Agent 日志文件路径 */
|
|
27
43
|
export declare function getAgentLogPath(dataDir: string, agentName: string): string;
|
|
28
|
-
/**
|
|
44
|
+
/** 只读检查 Agent PID;过期元数据必须在持有 lifecycle lock 的清理路径中删除。 */
|
|
29
45
|
export declare function getAgentPid(dataDir: string, agentName: string): number | undefined;
|
|
30
46
|
export declare function getRollCoreVersion(): string;
|
|
31
47
|
export declare function writeAgentRuntimeSidecar(agent: RegisteredAgent, dataDir: string, pid: number): void;
|
|
@@ -46,11 +62,16 @@ export declare function waitForAgentReady(agent: RegisteredAgent, options?: {
|
|
|
46
62
|
* - `stdio + on-demand`:保留给旧健康检查逻辑使用
|
|
47
63
|
* - `streamable-http + core-managed`:作为本地常驻后台服务启动
|
|
48
64
|
*/
|
|
49
|
-
export declare function startAgent(agent: RegisteredAgent, dataDir: string, env?: Readonly<Record<string, string
|
|
65
|
+
export declare function startAgent(agent: RegisteredAgent, dataDir: string, env?: Readonly<Record<string, string>>, options?: {
|
|
66
|
+
readonly lifecycleLock?: AgentLifecycleLock;
|
|
67
|
+
}): number;
|
|
50
68
|
/** 停止一个后台运行的 Agent */
|
|
51
69
|
export declare function stopAgent(dataDir: string, agentName: string): boolean;
|
|
52
70
|
export declare function stopAgentGracefully(dataDir: string, agentName: string, options?: {
|
|
53
71
|
readonly timeoutMs?: number;
|
|
54
72
|
readonly intervalMs?: number;
|
|
73
|
+
readonly expectedIdentity?: ManagedAgentRuntimeIdentity;
|
|
74
|
+
readonly lifecycleLock?: AgentLifecycleLock;
|
|
55
75
|
}): Promise<boolean>;
|
|
76
|
+
export declare function acquireAgentLifecycleLock(dataDir: string, agentName: string): AgentLifecycleLock;
|
|
56
77
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"cross-spawn";import{closeSync as e,existsSync as n,mkdirSync as r,openSync as i,readFileSync as o,unlinkSync as s,writeFileSync as a}from"node:fs";import{resolve as c,dirname as l}from"node:path";import{McpClientManager as m}from"../mcp/client-manager.js";import{readJsonFile as d}from"./json-file.js";import{resolveDevSpawnSpec as p}from"./dev-spawn.js";import{inferAgentSourceType as u}from"./source.js";const g=1,f="unknown",h=15e3,w=2e3,A=500;function $(t,e){return c(t,"pids",`${e}.pid`)}function k(t,e){return c(t,"pids",`${e}.runtime.json`)}function v(t,e){for(const r of[$(t,e),k(t,e)])n(r)&&s(r)}export function cleanupOrphanAgentRuntimeMetadata(t,e){return void 0===getAgentPid(t,e)&&(v(t,e),!0)}export function getAgentLogPath(t,e){return c(t,"logs",`${e}.log`)}function y(t){try{return process.kill(t,0),!0}catch{return!1}}export function getAgentPid(t,e){const r=$(t,e);if(!n(r))return;const i=Number(o(r,"utf-8").trim());if(!Number.isNaN(i)&&y(i))return i;v(t,e)}export function getRollCoreVersion(){try{const t=c(import.meta.dirname,"../../package.json"),e=d(t);return S(e)&&"string"==typeof e.version?e.version:f}catch{return f}}export function writeAgentRuntimeSidecar(t,e,i){const o=k(e,t.skill.name),s=l(o);n(s)||r(s,{recursive:!0});const c={schemaVersion:1,agentName:t.skill.name,pid:i,coreVersion:getRollCoreVersion(),startedAt:(new Date).toISOString(),..."streamable-http"===t.transport.type?{endpoint:t.transport.endpoint}:{}};a(o,`${JSON.stringify(c,null,2)}\n`,"utf-8")}export function inspectManagedAgentRuntime(t,e){const n=getAgentPid(e,t.skill.name),r=getRollCoreVersion(),i="streamable-http"===t.transport.type?t.transport.endpoint:void 0;if(void 0===n){const n=R(e,t.skill.name);return void 0!==n?{..."invalid"!==n?{sidecar:n}:{},expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[{code:"orphan-sidecar",message:"invalid"===n?"runtime sidecar 存在但没有活动 PID,且无法解析":`runtime sidecar 记录 PID ${String(n.pid)},但没有活动 PID`,fix:`运行 \`roll doctor --fix\` 清理 ${t.skill.name} 的过期 runtime 元数据`}]}:{expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[]}}const o=[],s=R(e,t.skill.name);return"invalid"===s?(o.push({code:"invalid-sidecar",message:"runtime sidecar 无法解析",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):s?(s.pid!==n&&o.push({code:"pid-mismatch",message:`runtime sidecar PID ${String(s.pid)} 与活动 PID ${String(n)} 不一致`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),s.coreVersion!==r&&o.push({code:"version-mismatch",message:`runtime sidecar 来自 core ${s.coreVersion},当前 core 是 ${r}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),i&&s.endpoint!==i&&o.push({code:"endpoint-mismatch",message:`runtime sidecar endpoint 是 ${s.endpoint??"n/a"},当前配置是 ${i}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,sidecar:s,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}):(o.push({code:"missing-sidecar",message:"进程存在但缺少 runtime sidecar",fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o})}export async function probeAgentEndpoint(t,e={}){const n=new m;try{const r=await n.connect(t.skill.name,t.transport,t.installPath,void 0!==e.timeoutMs?{timeoutMs:e.timeoutMs}:{});await r.listTools()}finally{await n.disconnectAll()}}function x(t){const e=process.env[t];if(void 0===e||0===e.trim().length)return;const n=Number.parseInt(e,10);return!Number.isInteger(n)||n<=0||String(n)!==e.trim()?void 0:n}export async function waitForAgentReady(t,e={}){const n=x("ROLL_AGENT_READY_STARTUP_TIMEOUT_MS")??e.startupTimeoutMs??h,r=x("ROLL_AGENT_READY_PROBE_TIMEOUT_MS")??e.probeTimeoutMs??w,i=x("ROLL_AGENT_READY_INTERVAL_MS")??e.intervalMs??A,o=Date.now()+n;let s;for(;Date.now()<o;)try{return void await probeAgentEndpoint(t,{timeoutMs:r})}catch(t){s=t,await P(i)}throw new Error(`Agent "${t.skill.name}" did not become ready within ${n}ms${s?`: ${s instanceof Error?s.message:String(s)}`:""}`)}export function startAgent(o,s,c){if("streamable-http"===o.transport.type&&"core-managed"!==o.runtime.ownership)throw new Error(`Agent "${o.skill.name}" 使用 streamable-http 传输且非 core-managed,请手动启动服务。\n 端点: ${o.transport.endpoint}`);const m=getAgentPid(s,o.skill.name);if(void 0!==m)throw new Error(`Agent "${o.skill.name}" 已在运行 (PID: ${String(m)})`);const d=E(o),p=getAgentLogPath(s,o.skill.name),u=l(p);n(u)||r(u,{recursive:!0});const g=i(p,"a"),f=t(d.command,[...d.args??[]],{cwd:o.installPath,detached:!0,windowsHide:!0,stdio:["ignore",g,g],...c?{env:{...process.env,...c}}:{}});if(e(g),!f.pid)throw new Error(`Failed to start agent "${o.skill.name}"`);const h=$(s,o.skill.name),w=l(h);n(w)||r(w,{recursive:!0});try{a(h,String(f.pid),"utf-8"),writeAgentRuntimeSidecar(o,s,f.pid)}catch(t){try{process.kill(f.pid,"SIGTERM")}catch{}throw v(s,o.skill.name),new Error(`Failed to persist runtime metadata for agent "${o.skill.name}"`,{cause:t})}return f.unref(),f.pid}export function stopAgent(t,e){const n=getAgentPid(t,e);if(void 0===n)return!1;try{process.kill(n,"SIGTERM")}catch{}return v(t,e),!0}export async function stopAgentGracefully(t,e,n={}){const r=getAgentPid(t,e);if(void 0===r)return!1;try{process.kill(r,"SIGTERM")}catch{return v(t,e),!0}const i=n.timeoutMs??15e3,o=n.intervalMs??200,s=Date.now()+i;for(;Date.now()<s;){if(!y(r))return v(t,e),!0;await P(o)}throw new Error(`Agent "${e}" did not stop within ${i}ms`)}function E(t){if("stdio"===t.transport.type){const e=p(t.transport.command,t.transport.args,t.installPath,u(t));return e||{command:t.transport.command,...t.transport.args?{args:t.transport.args}:{}}}if("core-managed"===t.runtime.ownership){const e=p(t.runtime.start.command,t.runtime.start.args,t.installPath,u(t));return e||{command:t.runtime.start.command,...t.runtime.start.args?{args:t.runtime.start.args}:{}}}throw new Error(`Agent "${t.skill.name}" does not have a managed runtime start command`)}function P(t){return new Promise(e=>{setTimeout(e,t)})}function R(t,e){const r=k(t,e);if(!n(r))return;let i;try{i=d(r)}catch{return"invalid"}return M(i)?i:"invalid"}function M(t){return!!S(t)&&(1===t.schemaVersion&&(!("string"!=typeof t.agentName||"number"!=typeof t.pid||!Number.isInteger(t.pid)||"string"!=typeof t.coreVersion||"string"!=typeof t.startedAt)&&(void 0===t.endpoint||"string"==typeof t.endpoint)))}function S(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}
|
|
1
|
+
import t from"cross-spawn";import{createHash as e,randomUUID as n}from"node:crypto";import{closeSync as r,existsSync as i,mkdirSync as o,openSync as s,readFileSync as a,statSync as c,unlinkSync as d,writeFileSync as u}from"node:fs";import{resolve as l,dirname as p}from"node:path";import{McpClientManager as m}from"../mcp/client-manager.js";import{readJsonFile as f}from"./json-file.js";import{isProcessStartToken as g,readProcessStartToken as y}from"./process-identity.js";import{resolveDevSpawnSpec as h}from"./dev-spawn.js";import{inferAgentSourceType as k}from"./source.js";const w=2,A="unknown",E=15e3,v=2e3,S=500,$=3e5,x=new WeakMap;export class AgentLifecycleBusyError extends Error{code="agent_lifecycle_busy";constructor(t){super(`Agent "${t}" 正在执行另一项生命周期操作,请稍后重试。`),this.name="AgentLifecycleBusyError"}}export class AgentRuntimeIdentityError extends Error{code="agent_runtime_identity_unverifiable";pid;constructor(t,e,n){super(`Agent "${t}" 的 runtime 身份无法安全验证 (PID: ${String(e)}):${n}。为避免停止无关进程,Roll 未发送信号且保留了 runtime 元数据。请先用系统工具确认并手动停止 PID ${String(e)},再运行 \`roll doctor --fix\` 清理过期元数据。`),this.name="AgentRuntimeIdentityError",this.pid=e}}function I(t,e){return l(t,"pids",`${e}.pid`)}function P(t,e){return l(t,"pids",`${e}.runtime.json`)}function R(t,n){const r=e("sha256").update(n).digest("hex");return l(t,"pids",`.${r}.lifecycle.lock`)}function T(t,e){for(const n of[I(t,e),P(t,e)])i(n)&&d(n)}export function cleanupOrphanAgentRuntimeMetadata(t,e){const n=acquireAgentLifecycleLock(t,e);try{return void 0===getAgentPid(t,e)&&(T(t,e),!0)}finally{n.release()}}export function getAgentLogPath(t,e){return l(t,"logs",`${e}.log`)}function M(t){try{return process.kill(t,0),!0}catch(t){return U(t,"EPERM")}}export function getAgentPid(t,e){const n=W(I(t,e));return void 0!==n&&M(n)?n:void 0}export function getRollCoreVersion(){try{const t=l(import.meta.dirname,"../../package.json"),e=f(t);return Z(e)&&"string"==typeof e.version?e.version:A}catch{return A}}export function writeAgentRuntimeSidecar(t,e,n){const r=y(n);if(void 0===r)throw new AgentRuntimeIdentityError(t.skill.name,n,"无法读取 OS 进程启动身份");const s=P(e,t.skill.name),a=p(s);i(a)||o(a,{recursive:!0});const c={schemaVersion:2,agentName:t.skill.name,pid:n,processStartToken:r,coreVersion:getRollCoreVersion(),startedAt:(new Date).toISOString(),..."streamable-http"===t.transport.type?{endpoint:t.transport.endpoint}:{}};u(s,`${JSON.stringify(c,null,2)}\n`,"utf-8")}export function inspectManagedAgentRuntime(t,e){const n=getAgentPid(e,t.skill.name),r=getRollCoreVersion(),i="streamable-http"===t.transport.type?t.transport.endpoint:void 0;if(void 0===n){const n=K(e,t.skill.name);return void 0!==n?{..."invalid"!==n?{sidecar:n}:{},expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[{code:"orphan-sidecar",message:"invalid"===n?"runtime sidecar 存在但没有活动 PID,且无法解析":`runtime sidecar 记录 PID ${String(n.pid)},但没有活动 PID`,fix:`运行 \`roll doctor --fix\` 清理 ${t.skill.name} 的过期 runtime 元数据`}]}:{expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:[]}}const o=[],s=K(e,t.skill.name);if("invalid"===s)return o.push({code:"invalid-sidecar",message:"runtime sidecar 无法解析",fix:D(t.skill.name,n)}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o};if(!s)return o.push({code:"missing-sidecar",message:"进程存在但缺少 runtime sidecar",fix:D(t.skill.name,n)}),{pid:n,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o};if(s.agentName!==t.skill.name&&o.push({code:"agent-name-mismatch",message:`runtime sidecar Agent 名称 ${s.agentName} 与当前 Agent ${t.skill.name} 不一致`,fix:D(t.skill.name,n)}),s.pid!==n&&o.push({code:"pid-mismatch",message:`runtime sidecar PID ${String(s.pid)} 与活动 PID ${String(n)} 不一致`,fix:D(t.skill.name,n)}),s.pid===n){const e=y(n);void 0===e?o.push({code:"process-identity-unavailable",message:`无法读取 PID ${String(n)} 的 OS 进程启动身份`,fix:D(t.skill.name,n)}):e!==s.processStartToken&&o.push({code:"process-identity-mismatch",message:`PID ${String(n)} 当前属于另一个进程实例,runtime 元数据已过期`,fix:D(t.skill.name,n)})}return s.coreVersion!==r&&o.push({code:"version-mismatch",message:`runtime sidecar 来自 core ${s.coreVersion},当前 core 是 ${r}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),i&&s.endpoint!==i&&o.push({code:"endpoint-mismatch",message:`runtime sidecar endpoint 是 ${s.endpoint??"n/a"},当前配置是 ${i}`,fix:`运行 \`roll agent stop ${t.skill.name}\` 后重新 \`roll agent start ${t.skill.name}\``}),{pid:n,sidecar:s,expectedCoreVersion:r,...i?{expectedEndpoint:i}:{},issues:o}}function D(t,e){return`先用系统工具确认并手动停止 PID ${String(e)},再运行 \`roll doctor --fix\` 清理 ${t} 的过期 runtime 元数据`}export async function probeAgentEndpoint(t,e={}){const n=new m;try{const r=await n.connect(t.skill.name,t.transport,t.installPath,void 0!==e.timeoutMs?{timeoutMs:e.timeoutMs}:{});await r.listTools()}finally{await n.disconnectAll()}}function N(t){const e=process.env[t];if(void 0===e||0===e.trim().length)return;const n=Number.parseInt(e,10);return!Number.isInteger(n)||n<=0||String(n)!==e.trim()?void 0:n}export async function waitForAgentReady(t,e={}){const n=N("ROLL_AGENT_READY_STARTUP_TIMEOUT_MS")??e.startupTimeoutMs??E,r=N("ROLL_AGENT_READY_PROBE_TIMEOUT_MS")??e.probeTimeoutMs??v,i=N("ROLL_AGENT_READY_INTERVAL_MS")??e.intervalMs??S,o=Date.now()+n;let s;for(;Date.now()<o;)try{return void await probeAgentEndpoint(t,{timeoutMs:r})}catch(t){s=t,await z(i)}throw new Error(`Agent "${t.skill.name}" did not become ready within ${n}ms${s?`: ${s instanceof Error?s.message:String(s)}`:""}`)}export function startAgent(t,e,n,r={}){const i=j(e,t.skill.name,r.lifecycleLock);try{return L(t,e,n)}finally{i.acquired&&i.lock.release()}}function L(e,n,a){if("streamable-http"===e.transport.type&&"core-managed"!==e.runtime.ownership)throw new Error(`Agent "${e.skill.name}" 使用 streamable-http 传输且非 core-managed,请手动启动服务。\n 端点: ${e.transport.endpoint}`);const c=getAgentPid(n,e.skill.name);if(void 0!==c)throw new Error(`Agent "${e.skill.name}" 已在运行 (PID: ${String(c)})`);const d=X(e),l=getAgentLogPath(n,e.skill.name),m=p(l);i(m)||o(m,{recursive:!0});const f=s(l,"a"),g=t(d.command,[...d.args??[]],{cwd:e.installPath,detached:!0,windowsHide:!0,stdio:["ignore",f,f],...a?{env:{...process.env,...a}}:{}});if(r(f),!g.pid)throw new Error(`Failed to start agent "${e.skill.name}"`);const y=I(n,e.skill.name),h=p(y);i(h)||o(h,{recursive:!0});try{u(y,String(g.pid),"utf-8"),writeAgentRuntimeSidecar(e,n,g.pid)}catch(t){try{g.kill("SIGTERM")}catch{}throw T(n,e.skill.name),new Error(`Failed to persist runtime metadata for agent "${e.skill.name}"`,{cause:t})}return g.unref(),g.pid}export function stopAgent(t,e){const n=acquireAgentLifecycleLock(t,e);try{return b(t,e)}finally{n.release()}}function b(t,e){const n=getAgentPid(t,e);if(void 0===n)return T(t,e),!1;const r=O(t,e,n);try{V(e,r)}catch(t){if(!U(t,"ESRCH"))throw t}return Y(t,e,r),!0}export async function stopAgentGracefully(t,e,n={}){const r=j(t,e,n.lifecycleLock);try{return await _(t,e,n)}finally{r.acquired&&r.lock.release()}}async function _(t,e,n){const r=getAgentPid(t,e);if(void 0===r)return T(t,e),!1;const i=O(t,e,r);if(void 0!==n.expectedIdentity&&!C(i,n.expectedIdentity))return!1;try{V(e,i)}catch(n){if(!U(n,"ESRCH"))throw n;return Y(t,e,i),!0}const o=n.timeoutMs??15e3,s=n.intervalMs??200,a=Date.now()+o;for(;Date.now()<a;){if(!M(r))return Y(t,e,i),!0;const n=y(r);if(void 0!==n&&n!==i.processStartToken)return Y(t,e,i),!0;await z(s)}throw new Error(`Agent "${e}" did not stop within ${o}ms`)}function O(t,e,n){const r=K(t,e);if(void 0===r)throw new AgentRuntimeIdentityError(e,n,"缺少 runtime sidecar");if("invalid"===r)throw new AgentRuntimeIdentityError(e,n,"runtime sidecar 无效或来自不含 processStartToken 的旧版本");if(r.agentName!==e||r.pid!==n)throw new AgentRuntimeIdentityError(e,n,"runtime sidecar 与当前 Agent/PID 不一致");const i=y(n);if(void 0===i)throw new AgentRuntimeIdentityError(e,n,"无法读取 OS 进程启动身份");if(i!==r.processStartToken)throw new AgentRuntimeIdentityError(e,n,"PID 当前属于另一个进程实例");return{pid:n,processStartToken:r.processStartToken,startedAt:r.startedAt}}function V(t,e){const n=y(e.pid);if(void 0===n)throw new AgentRuntimeIdentityError(t,e.pid,"发送信号前无法重新验证 OS 身份");if(n!==e.processStartToken)throw new AgentRuntimeIdentityError(t,e.pid,"发送信号前 PID 已被其他进程复用");process.kill(e.pid,"SIGTERM")}function C(t,e){return t.pid===e.pid&&t.processStartToken===e.processStartToken&&t.startedAt===e.startedAt}export function acquireAgentLifecycleLock(t,e){const r=l(t),i=R(r,e);o(p(i),{recursive:!0});const s=n(),a=y(process.pid);if(void 0===a)throw new Error(`无法验证当前 Roll 进程 (PID: ${String(process.pid)}) 的 OS 启动身份,拒绝获取 Agent lifecycle lock。`);const c={pid:process.pid,processStartToken:a,token:s,createdAtMs:Date.now()};for(let t=0;t<2;t+=1)try{u(i,`${JSON.stringify(c)}\n`,{encoding:"utf-8",flag:"wx",mode:384});const t={release:()=>q(t)};return x.set(t,{dataDir:r,agentName:e,lockPath:i,token:s,released:!1}),t}catch(n){if(!U(n,"EEXIST"))throw n;if(0===t&&G(i))continue;throw new AgentLifecycleBusyError(e)}throw new AgentLifecycleBusyError(e)}function j(t,e,n){if(void 0===n)return{lock:acquireAgentLifecycleLock(t,e),acquired:!0};const r=x.get(n);if(void 0===r||r.released||r.dataDir!==l(t)||r.agentName!==e)throw new Error("Invalid Agent lifecycle lock handle.");return{lock:n,acquired:!1}}function q(t){const e=x.get(t);if(void 0!==e&&!e.released){e.released=!0,x.delete(t);try{const t=B(e.lockPath);t?.token===e.token&&d(e.lockPath)}catch{}}}function G(t){let e;try{e=a(t,"utf-8")}catch{return!1}const n=F(e);let r;try{r=Date.now()-(n?.createdAtMs??c(t).mtimeMs)}catch{return!1}if(!(void 0===n?r>$:H(n)))return!1;try{return a(t,"utf-8")===e&&(d(t),!0)}catch{return!1}}function B(t){if(i(t))return F(a(t,"utf-8"))}function F(t){let e;try{e=JSON.parse(t)}catch{return}if(Z(e)&&"number"==typeof e.pid&&Number.isInteger(e.pid)&&!(e.pid<=0)&&g(e.processStartToken)&&"string"==typeof e.token&&"number"==typeof e.createdAtMs&&Number.isFinite(e.createdAtMs))return{pid:e.pid,processStartToken:e.processStartToken,token:e.token,createdAtMs:e.createdAtMs}}function H(t){if(!J(t.pid))return!0;const e=y(t.pid);return void 0!==e&&e!==t.processStartToken}function J(t){try{return process.kill(t,0),!0}catch(t){return U(t,"EPERM")}}function U(t,e){return t instanceof Error&&"code"in t&&t.code===e}function Y(t,e,n){const r=I(t,e);if(W(r)!==n.pid)return!1;const i=P(t,e),o=K(t,e);if(void 0===o||"invalid"===o||o.agentName!==e||!C(o,n))return!1;try{return d(r),d(i),!0}catch(t){if(U(t,"ENOENT"))return!1;throw t}}function W(t){try{const e=Number(a(t,"utf-8").trim());return Number.isInteger(e)&&e>0?e:void 0}catch{return}}function X(t){if("stdio"===t.transport.type){const e=h(t.transport.command,t.transport.args,t.installPath,k(t));return e||{command:t.transport.command,...t.transport.args?{args:t.transport.args}:{}}}if("core-managed"===t.runtime.ownership){const e=h(t.runtime.start.command,t.runtime.start.args,t.installPath,k(t));return e||{command:t.runtime.start.command,...t.runtime.start.args?{args:t.runtime.start.args}:{}}}throw new Error(`Agent "${t.skill.name}" does not have a managed runtime start command`)}function z(t){return new Promise(e=>{setTimeout(e,t)})}function K(t,e){const n=P(t,e);if(!i(n))return;let r;try{r=f(n)}catch{return"invalid"}return Q(r)?r:"invalid"}function Q(t){return!!Z(t)&&(2===t.schemaVersion&&(!("string"!=typeof t.agentName||"number"!=typeof t.pid||!Number.isInteger(t.pid)||!g(t.processStartToken)||"string"!=typeof t.coreVersion||"string"!=typeof t.startedAt)&&(void 0===t.endpoint||"string"==typeof t.endpoint)))}function Z(t){return"object"==typeof t&&null!==t&&!Array.isArray(t)}
|
package/dist/registry/store.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{writeFileSync as t,existsSync as e,mkdirSync as n,renameSync as r}from"node:fs";import{resolve as i,dirname as s}from"node:path";import{readJsonFile as
|
|
1
|
+
import{writeFileSync as t,existsSync as e,mkdirSync as n,renameSync as r}from"node:fs";import{resolve as i,dirname as s}from"node:path";import{readJsonFile as o}from"./json-file.js";import{inferAgentSourceFromInstallPath as a}from"./source.js";import{AGENT_STATUSES as u,AGENT_STORE_SCHEMA_VERSION as c,AGENT_ENV_VALUE_TYPES as l,createDefaultRuntimeForTransport as p}from"../types/agent.js";const f="agents.json";export class AgentStore{storePath;constructor(t){this.storePath=i(t,f)}list(){return this.load().agents}findByName(t){return this.list().find(e=>e.skill.name===t)}add(t){const e=[...this.list()];if(-1!==e.findIndex(e=>e.skill.name===t.skill.name))throw new Error(`Agent "${t.skill.name}" is already registered`);e.push(t),this.save(e)}remove(t){const e=this.list(),n=e.filter(e=>e.skill.name!==t);return n.length!==e.length&&(this.save([...n]),!0)}replace(t,e){const n=[...this.list()],r=n.findIndex(e=>e.skill.name===t);if(-1===r)return!1;if(-1!==n.findIndex((t,n)=>n!==r&&t.skill.name===e.skill.name))throw new Error(`Agent "${e.skill.name}" is already registered`);return n[r]=e,this.save(n),!0}updateStatus(t,e){const n=this.list().map(n=>n.skill.name===t?{...n,status:e}:n);this.save([...n])}save(i){const o=s(this.storePath);e(o)||n(o,{recursive:!0});const a={schemaVersion:c,agents:i},u=`${this.storePath}.tmp`;t(u,JSON.stringify(a,null,2),"utf-8"),r(u,this.storePath)}load(){if(!e(this.storePath))return d();let t;try{t=o(this.storePath)}catch{return d()}return m(t)}}function d(){return{schemaVersion:c,agents:[]}}function m(t){if(Array.isArray(t))return{schemaVersion:c,agents:t.flatMap(t=>{const e=h(t);return e?[e]:[]})};if(!B(t))return d();const e=t.agents;return Array.isArray(e)?{schemaVersion:t.schemaVersion===c?c:2,agents:e.flatMap(t=>{const e=h(t);return e?[e]:[]})}:d()}function h(t){if(!B(t))return;const e=g(t.skill),n=w(t.transport),r=D(t.installPath);if(!e||!n||!r)return;const i=b(t.runtime,n),s=V(t.source,r,n),o=D(t.registeredAt)??new Date(0).toISOString(),a=x(t.status),u=D(t.skillBody);return{skill:e,transport:n,runtime:i,installPath:r,registeredAt:o,status:a,...s?{source:s}:{},...u?{skillBody:u}:{}}}function g(t){if(!B(t))return;const e=D(t.name),n=D(t.description);if(!e||!n)return;const r=D(t.license),i=D(t.compatibility),s=v(t.env);return{name:e,description:n,...r?{license:r}:{},...i?{compatibility:i}:{},metadata:y(t.metadata),...s?{env:s}:{}}}function y(t){if(!B(t))return{};const e={};for(const[n,r]of Object.entries(t))e[n]=String(r);return e}function v(t){if(!B(t))return;const e=k(t.required),n=k(t.optional);return e||n?{...e?{required:e}:{},...n?{optional:n}:{}}:void 0}function k(t){if(!Array.isArray(t))return;const e=t.flatMap(t=>{if(!B(t))return[];const e=D(t.name);if(!e)return[];const n=D(t.purpose),r=D(t.example),i=D(t.default),s=D(t.type),o=l.find(t=>t===s),a=I(t.secret),u=I(t.configurable),c=N(t.sourcePath);return[{name:e,...n?{purpose:n}:{},...r?{example:r}:{},...i?{default:i}:{},...void 0!==o?{type:o}:{},...void 0!==a?{secret:a}:{},...void 0!==u?{configurable:u}:{},...void 0!==c&&c.length>0?{sourcePath:c}:{}}]});return e.length>0?e:void 0}function w(t){if(!B(t))return;const e=D(t.type);if("stdio"===e){const n=D(t.command);if(!n)return;const r=N(t.args);return r?{type:e,command:n,args:r}:{type:e,command:n}}if("streamable-http"===e){const n=D(t.endpoint);if(!n)return;return{type:e,endpoint:n}}}function b(t,e){if(!B(t))return p(e);const n=D(t.ownership);if("on-demand"===n)return{ownership:n};if("external-managed"===n)return{ownership:n};if("core-managed"===n&&"streamable-http"===e.type){const e=A(t.start),r=P(t.endpoint);if(e&&r){const i=S(t.setup);return i?{ownership:n,start:e,endpoint:r,setup:i}:{ownership:n,start:e,endpoint:r}}}return p(e)}function A(t){if(!B(t))return;const e=D(t.command);if(!e)return;const n=N(t.args);return n?{command:e,args:n}:{command:e}}function P(t){if(!B(t))return;const e=D(t.path),n="number"==typeof t.port?t.port:void 0;return e&&void 0!==n&&Number.isInteger(n)?{path:e,port:n}:void 0}function S(t){if(!B(t))return;const e=t.playwright;if(!B(e))return;const n=N(e.browsers);return n&&0!==n.length?{playwright:{browsers:n}}:void 0}function V(t,e,n){if(!B(t))return a(e,n);const r=D(t.type);if(!r)return a(e,n);switch(r){case"git":return M(D(t.url));case"local-path":return{type:r,path:D(t.path)??e};case"installed-package":{const e=D(t.packageName),n=D(t.packageSpec),i=D(t.installDir),s=D(t.installedVersion);if(!e||!n||!i)return;return{type:r,packageName:e,packageSpec:n,installDir:i,...s?{installedVersion:s}:{}}}case"remote-manifest":{const e=D(t.endpoint)??("streamable-http"===n.type?n.endpoint:void 0);return e?{type:r,endpoint:e}:void 0}default:return j(r,t,e,n)}}function j(t,e,n,r){switch(t){case"git":return M(D(e.url));case"local":return{type:"local-path",path:D(e.path)??n};case"installed":{const t=D(e.packageName),n=D(e.packageSpec),r=D(e.installDir),i=D(e.installedVersion);if(!t||!n||!r)return;return{type:"installed-package",packageName:t,packageSpec:n,installDir:r,...i?{installedVersion:i}:{}}}case"remote":{const t=a(n,r);if(t&&"remote-manifest"!==t.type)return t;const i=D(e.endpoint)??("streamable-http"===r.type?r.endpoint:void 0);return i?{type:"remote-manifest",endpoint:i}:void 0}}}function x(t){return"string"==typeof t&&u.includes(t)?t:"idle"}function N(t){if(!Array.isArray(t))return;return t.filter(t=>"string"==typeof t)}function D(t){return"string"==typeof t&&t.length>0?t:void 0}function I(t){return"boolean"==typeof t?t:void 0}function B(t){return"object"==typeof t&&null!==t}function M(t){return t?{type:"git",url:t}:{type:"git"}}
|
package/dist/types/agent.d.ts
CHANGED
|
@@ -8,11 +8,17 @@ export type AgentTransport = {
|
|
|
8
8
|
readonly endpoint: string;
|
|
9
9
|
};
|
|
10
10
|
/** SKILL.md frontmatter 解析结果 */
|
|
11
|
+
export declare const AGENT_ENV_VALUE_TYPES: readonly ["string", "boolean", "number", "json", "url"];
|
|
12
|
+
export type AgentEnvValueType = (typeof AGENT_ENV_VALUE_TYPES)[number];
|
|
11
13
|
export interface AgentEnvDeclaration {
|
|
12
14
|
readonly name: string;
|
|
13
15
|
readonly purpose?: string;
|
|
14
16
|
readonly example?: string;
|
|
15
17
|
readonly default?: string;
|
|
18
|
+
readonly type?: AgentEnvValueType;
|
|
19
|
+
readonly secret?: boolean;
|
|
20
|
+
readonly configurable?: boolean;
|
|
21
|
+
readonly sourcePath?: readonly string[];
|
|
16
22
|
}
|
|
17
23
|
export interface AgentSkillEnvDeclarations {
|
|
18
24
|
readonly required?: readonly AgentEnvDeclaration[];
|
package/dist/types/agent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_STATUSES=["idle","starting","online","error","stopped"];export const AGENT_SOURCE_TYPES=["git","local-path","installed-package","remote-manifest"];export const AGENT_RUNTIME_OWNERSHIPS=["on-demand","core-managed","external-managed"];export const AGENT_STORE_SCHEMA_VERSION=2;export function createDefaultRuntimeForTransport(e){return"stdio"===e.type?{ownership:"on-demand"}:{ownership:"external-managed"}}
|
|
1
|
+
export const AGENT_ENV_VALUE_TYPES=["string","boolean","number","json","url"];export const AGENT_STATUSES=["idle","starting","online","error","stopped"];export const AGENT_SOURCE_TYPES=["git","local-path","installed-package","remote-manifest"];export const AGENT_RUNTIME_OWNERSHIPS=["on-demand","core-managed","external-managed"];export const AGENT_STORE_SCHEMA_VERSION=2;export function createDefaultRuntimeForTransport(e){return"stdio"===e.type?{ownership:"on-demand"}:{ownership:"external-managed"}}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ConfigApplicationService } from "../config/application-service.ts";
|
|
2
|
+
import type { RollUiController } from "./contracts.ts";
|
|
3
|
+
type ConfigApplicationPort = Pick<ConfigApplicationService, "read" | "previewStructured" | "previewYaml" | "saveStructured" | "saveYaml">;
|
|
4
|
+
export interface ConfigApplicationUiControllerOptions {
|
|
5
|
+
readonly config: ConfigApplicationPort;
|
|
6
|
+
readonly getCatalog: RollUiController["getCatalog"];
|
|
7
|
+
readonly getAgentStatus?: RollUiController["getAgentStatus"];
|
|
8
|
+
readonly applyAgentEffects?: RollUiController["applyAgentEffects"];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Reuses ConfigApplicationService for both structured and raw-YAML editing.
|
|
12
|
+
* Agent lifecycle hooks remain injectable until the lifecycle layer is wired.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createConfigApplicationUiController(options: ConfigApplicationUiControllerOptions): RollUiController;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export function createConfigApplicationUiController(n){return{getConfig:()=>n.config.read(),getCatalog:()=>n.getCatalog(),getAgentStatus:()=>n.getAgentStatus?.()??{available:!1,agents:[]},previewConfig:t=>e(n.config,t),saveConfig:e=>t(n.config,e),applyAgentEffects:e=>n.applyAgentEffects?.(e)??i(e)}}function e(e,t){return"structured"===t.mode?e.previewStructured(t.persisted,t.expectedRevision):e.previewYaml(t.yaml,t.expectedRevision)}function t(e,t){return"structured"===t.mode?e.saveStructured(t.persisted,t.expectedRevision):e.saveYaml(t.yaml,t.expectedRevision)}function i(e){return{available:!1,applied:[],skipped:e.effects,reason:"Agent lifecycle adapter is not configured."}}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ConfigActivationEffect } from "../config/application-service.ts";
|
|
2
|
+
import type { ConfigRevision } from "../config/document-store.ts";
|
|
3
|
+
export declare const ROLL_UI_CONFIG_EDIT_MODES: readonly ["structured", "yaml"];
|
|
4
|
+
export type RollUiConfigEditMode = (typeof ROLL_UI_CONFIG_EDIT_MODES)[number];
|
|
5
|
+
export interface RollUiStructuredConfigRequest {
|
|
6
|
+
readonly mode: "structured";
|
|
7
|
+
readonly persisted: unknown;
|
|
8
|
+
readonly expectedRevision?: ConfigRevision;
|
|
9
|
+
}
|
|
10
|
+
export interface RollUiYamlConfigRequest {
|
|
11
|
+
readonly mode: "yaml";
|
|
12
|
+
readonly yaml: string;
|
|
13
|
+
readonly expectedRevision?: ConfigRevision;
|
|
14
|
+
}
|
|
15
|
+
export type RollUiConfigRequest = RollUiStructuredConfigRequest | RollUiYamlConfigRequest;
|
|
16
|
+
export type RollUiSaveConfigRequest = (Omit<RollUiStructuredConfigRequest, "expectedRevision"> & {
|
|
17
|
+
readonly expectedRevision: ConfigRevision;
|
|
18
|
+
}) | (Omit<RollUiYamlConfigRequest, "expectedRevision"> & {
|
|
19
|
+
readonly expectedRevision: ConfigRevision;
|
|
20
|
+
});
|
|
21
|
+
export interface RollUiApplyEffectsRequest {
|
|
22
|
+
/**
|
|
23
|
+
* Untrusted client input. The lifecycle adapter must reconcile these effects
|
|
24
|
+
* with the latest saved activation plan before changing process state.
|
|
25
|
+
*/
|
|
26
|
+
readonly effects: readonly ConfigActivationEffect[];
|
|
27
|
+
}
|
|
28
|
+
type Awaitable<T> = T | Promise<T>;
|
|
29
|
+
/**
|
|
30
|
+
* HTTP-independent application boundary used by the local UI server.
|
|
31
|
+
* Implementations can be tested without opening a socket.
|
|
32
|
+
*/
|
|
33
|
+
export interface RollUiController {
|
|
34
|
+
getConfig(): Awaitable<unknown>;
|
|
35
|
+
getCatalog(): Awaitable<unknown>;
|
|
36
|
+
getAgentStatus(): Awaitable<unknown>;
|
|
37
|
+
previewConfig(request: RollUiConfigRequest): Awaitable<unknown>;
|
|
38
|
+
saveConfig(request: RollUiSaveConfigRequest): Awaitable<unknown>;
|
|
39
|
+
applyAgentEffects(request: RollUiApplyEffectsRequest): Awaitable<unknown>;
|
|
40
|
+
}
|
|
41
|
+
export interface RollUiStaticAsset {
|
|
42
|
+
readonly body: Uint8Array | string;
|
|
43
|
+
readonly contentType: string;
|
|
44
|
+
}
|
|
45
|
+
/** Receives a normalized absolute URL path such as `/index.html`. */
|
|
46
|
+
export interface RollUiStaticAssetProvider {
|
|
47
|
+
getAsset(pathname: string): Awaitable<RollUiStaticAsset | null>;
|
|
48
|
+
}
|
|
49
|
+
export {};
|