@caupulican/pi-adaptative 0.81.26 → 0.81.27
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/CHANGELOG.md +31 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +3 -2
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +4 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +19 -2
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +21 -10
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +71 -33
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/bounded-completion.d.ts.map +1 -1
- package/dist/core/autonomy/bounded-completion.js +59 -19
- package/dist/core/autonomy/bounded-completion.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts +31 -0
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -0
- package/dist/core/autonomy/lane-tool-surface.js +96 -0
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -0
- package/dist/core/background-lane-controller.d.ts +6 -3
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +134 -25
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/billing-failover-controller.d.ts.map +1 -1
- package/dist/core/billing-failover-controller.js +1 -1
- package/dist/core/billing-failover-controller.js.map +1 -1
- package/dist/core/context/current-work-memory.d.ts +9 -0
- package/dist/core/context/current-work-memory.d.ts.map +1 -0
- package/dist/core/context/current-work-memory.js +61 -0
- package/dist/core/context/current-work-memory.js.map +1 -0
- package/dist/core/context/file-store-memory-provider.d.ts +9 -0
- package/dist/core/context/file-store-memory-provider.d.ts.map +1 -0
- package/dist/core/context/file-store-memory-provider.js +106 -0
- package/dist/core/context/file-store-memory-provider.js.map +1 -0
- package/dist/core/context/long-term-memory-trigger.d.ts +15 -0
- package/dist/core/context/long-term-memory-trigger.d.ts.map +1 -0
- package/dist/core/context/long-term-memory-trigger.js +39 -0
- package/dist/core/context/long-term-memory-trigger.js.map +1 -0
- package/dist/core/context/memory-prompt-block.d.ts +3 -1
- package/dist/core/context/memory-prompt-block.d.ts.map +1 -1
- package/dist/core/context/memory-prompt-block.js +20 -3
- package/dist/core/context/memory-prompt-block.js.map +1 -1
- package/dist/core/context/memory-prompt-budget.d.ts +17 -0
- package/dist/core/context/memory-prompt-budget.d.ts.map +1 -0
- package/dist/core/context/memory-prompt-budget.js +64 -0
- package/dist/core/context/memory-prompt-budget.js.map +1 -0
- package/dist/core/context/memory-provider-contract.d.ts.map +1 -1
- package/dist/core/context/memory-provider-contract.js +2 -5
- package/dist/core/context/memory-provider-contract.js.map +1 -1
- package/dist/core/context/memory-tier-composer.d.ts +27 -0
- package/dist/core/context/memory-tier-composer.d.ts.map +1 -0
- package/dist/core/context/memory-tier-composer.js +79 -0
- package/dist/core/context/memory-tier-composer.js.map +1 -0
- package/dist/core/context-gc.d.ts.map +1 -1
- package/dist/core/context-gc.js +1 -0
- package/dist/core/context-gc.js.map +1 -1
- package/dist/core/cost/cost-summary.d.ts +3 -1
- package/dist/core/cost/cost-summary.d.ts.map +1 -1
- package/dist/core/cost/cost-summary.js +3 -3
- package/dist/core/cost/cost-summary.js.map +1 -1
- package/dist/core/cost-guard.d.ts +17 -7
- package/dist/core/cost-guard.d.ts.map +1 -1
- package/dist/core/cost-guard.js +33 -10
- package/dist/core/cost-guard.js.map +1 -1
- package/dist/core/default-tool-surface.d.ts +9 -0
- package/dist/core/default-tool-surface.d.ts.map +1 -0
- package/dist/core/default-tool-surface.js +18 -0
- package/dist/core/default-tool-surface.js.map +1 -0
- package/dist/core/delegation/worker-runner.d.ts +11 -5
- package/dist/core/delegation/worker-runner.d.ts.map +1 -1
- package/dist/core/delegation/worker-runner.js +24 -12
- package/dist/core/delegation/worker-runner.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +15 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +2 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +7 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +9 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/failure-corpus.d.ts.map +1 -1
- package/dist/core/failure-corpus.js +2 -4
- package/dist/core/failure-corpus.js.map +1 -1
- package/dist/core/footer-data-provider.d.ts.map +1 -1
- package/dist/core/footer-data-provider.js +9 -1
- package/dist/core/footer-data-provider.js.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.d.ts.map +1 -1
- package/dist/core/goals/goal-continuation-prompt.js +4 -3
- package/dist/core/goals/goal-continuation-prompt.js.map +1 -1
- package/dist/core/http-dispatcher.d.ts +12 -0
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +42 -4
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/memory/memory-manager.d.ts +8 -3
- package/dist/core/memory/memory-manager.d.ts.map +1 -1
- package/dist/core/memory/memory-manager.js +27 -11
- package/dist/core/memory/memory-manager.js.map +1 -1
- package/dist/core/memory/memory-provider.d.ts +5 -1
- package/dist/core/memory/memory-provider.d.ts.map +1 -1
- package/dist/core/memory/memory-provider.js.map +1 -1
- package/dist/core/memory/providers/file-store.d.ts +3 -1
- package/dist/core/memory/providers/file-store.d.ts.map +1 -1
- package/dist/core/memory/providers/file-store.js +7 -2
- package/dist/core/memory/providers/file-store.js.map +1 -1
- package/dist/core/memory/providers/transcript-recall.d.ts +1 -0
- package/dist/core/memory/providers/transcript-recall.d.ts.map +1 -1
- package/dist/core/memory/providers/transcript-recall.js +4 -7
- package/dist/core/memory/providers/transcript-recall.js.map +1 -1
- package/dist/core/memory-controller.d.ts +36 -10
- package/dist/core/memory-controller.d.ts.map +1 -1
- package/dist/core/memory-controller.js +137 -21
- package/dist/core/memory-controller.js.map +1 -1
- package/dist/core/model-registry.d.ts +23 -0
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +87 -8
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +30 -11
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/model-router-controller.d.ts.map +1 -1
- package/dist/core/model-router-controller.js +33 -29
- package/dist/core/model-router-controller.js.map +1 -1
- package/dist/core/model-selection-controller.d.ts +3 -0
- package/dist/core/model-selection-controller.d.ts.map +1 -1
- package/dist/core/model-selection-controller.js +8 -1
- package/dist/core/model-selection-controller.js.map +1 -1
- package/dist/core/models/perf-profile.d.ts.map +1 -1
- package/dist/core/models/perf-profile.js +95 -33
- package/dist/core/models/perf-profile.js.map +1 -1
- package/dist/core/profile-filter-controller.d.ts +11 -0
- package/dist/core/profile-filter-controller.d.ts.map +1 -1
- package/dist/core/profile-filter-controller.js +56 -11
- package/dist/core/profile-filter-controller.js.map +1 -1
- package/dist/core/profile-registry.d.ts +2 -2
- package/dist/core/profile-registry.d.ts.map +1 -1
- package/dist/core/profile-registry.js +58 -7
- package/dist/core/profile-registry.js.map +1 -1
- package/dist/core/reflection-controller.d.ts +7 -7
- package/dist/core/reflection-controller.d.ts.map +1 -1
- package/dist/core/reflection-controller.js +78 -12
- package/dist/core/reflection-controller.js.map +1 -1
- package/dist/core/research/research-runner.d.ts +2 -2
- package/dist/core/research/research-runner.d.ts.map +1 -1
- package/dist/core/research/research-runner.js +8 -6
- package/dist/core/research/research-runner.js.map +1 -1
- package/dist/core/resource-loader.d.ts +4 -2
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +11 -1
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/runtime-builder.d.ts +13 -1
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +127 -35
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/sdk.d.ts +8 -7
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +35 -5
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/security/secret-text.d.ts +3 -0
- package/dist/core/security/secret-text.d.ts.map +1 -0
- package/dist/core/security/secret-text.js +26 -0
- package/dist/core/security/secret-text.js.map +1 -0
- package/dist/core/settings-manager.d.ts +59 -17
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +300 -51
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +1 -1
- package/dist/core/skills.js.map +1 -1
- package/dist/core/system-prompt-builder.d.ts +7 -0
- package/dist/core/system-prompt-builder.d.ts.map +1 -1
- package/dist/core/system-prompt-builder.js +23 -1
- package/dist/core/system-prompt-builder.js.map +1 -1
- package/dist/core/tools/delegate.d.ts.map +1 -1
- package/dist/core/tools/delegate.js +15 -11
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +2 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +8 -1
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +69 -29
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/thinking-selector.js +2 -0
- package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/dist/modes/interactive/config-backup.d.ts +7 -1
- package/dist/modes/interactive/config-backup.d.ts.map +1 -1
- package/dist/modes/interactive/config-backup.js +196 -32
- package/dist/modes/interactive/config-backup.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +10 -28
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/local-model-commands.d.ts.map +1 -1
- package/dist/modes/interactive/local-model-commands.js +2 -0
- package/dist/modes/interactive/local-model-commands.js.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.d.ts +6 -4
- package/dist/modes/interactive/profile-menu-controller.d.ts.map +1 -1
- package/dist/modes/interactive/profile-menu-controller.js +248 -65
- package/dist/modes/interactive/profile-menu-controller.js.map +1 -1
- package/dist/modes/interactive/report-commands.d.ts.map +1 -1
- package/dist/modes/interactive/report-commands.js +8 -2
- package/dist/modes/interactive/report-commands.js.map +1 -1
- package/dist/modes/interactive/settings-selector-flow.d.ts.map +1 -1
- package/dist/modes/interactive/settings-selector-flow.js +8 -0
- package/dist/modes/interactive/settings-selector-flow.js.map +1 -1
- package/dist/modes/interactive/theme/theme-schema.json +1 -1
- package/dist/modes/interactive/theme/theme.d.ts +1 -1
- package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/dist/modes/interactive/theme/theme.js +3 -1
- package/dist/modes/interactive/theme/theme.js.map +1 -1
- package/docs/extensions.md +32 -0
- package/docs/resources.md +15 -7
- package/docs/settings.md +54 -5
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/preset.ts +2 -2
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +44 -35
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perf-profile.d.ts","sourceRoot":"","sources":["../../../src/core/models/perf-profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EACN,KAAK,GAAG,EAER,KAAK,OAAO,EAEZ,KAAK,KAAK,EAEV,MAAM,mBAAmB,CAAC;AAI3B,eAAO,MAAM,uCAAuC,QAAiB,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,iBAAiB,CAAC;IACxB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,8BAA8B;IAC9C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IACpD,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAClE,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAa5E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAMzE;AAED,wBAAgB,sBAAsB,CACrC,OAAO,EAAE,gBAAgB,GAAG,SAAS,EACrC,MAAM,EAAE,eAAe,EACvB,EAAE,GAAE,MAA8C,GAChD,gBAAgB,GAAG,SAAS,CAyB9B;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0B3G;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAQpE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,8BAA8B,GAAG,QAAQ,CA+C3G","sourcesContent":["import type { StreamFn, StreamIdleOptions } from \"@caupulican/pi-agent-core\";\nimport {\n\ttype Api,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\n\nconst PERF_EWMA_ALPHA = 0.3;\nconst CHARS_PER_TOKEN_ESTIMATE = 4;\nexport const DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS = 30 * 60 * 1000;\n\nexport interface ModelPerfProfile {\n\tprefillTokensPerSecond?: number;\n\tdecodeTokensPerSecond?: number;\n\tloadMs?: number;\n\tsamples: number;\n\tupdatedAt: string;\n}\n\nexport interface ModelPerfSample {\n\tpromptTokens?: number;\n\tcompletionTokens?: number;\n\theadersToFirstTokenMs?: number;\n\trequestToFirstTokenMs?: number;\n\tfirstTokenToDoneMs?: number;\n\tloadMs?: number;\n\tat?: string;\n}\n\nexport interface AdaptiveStreamIdleInput {\n\tbase: StreamIdleOptions;\n\tprofile?: ModelPerfProfile;\n\tpromptTokens: number;\n\tceilingMs?: number;\n\tlocalClass?: boolean;\n}\n\nexport interface ModelPerfProfileStreamRecorder {\n\tmodelKey: (model: Model<Api>) => string | undefined;\n\trecordSample: (modelKey: string, sample: ModelPerfSample) => void;\n\tnowMs?: () => number;\n\tnowIso?: () => string;\n}\n\nexport function isModelPerfProfile(value: unknown): value is ModelPerfProfile {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst record = value as Record<string, unknown>;\n\tconst samples = record.samples;\n\treturn (\n\t\t(record.prefillTokensPerSecond === undefined || isPositiveFiniteNumber(record.prefillTokensPerSecond)) &&\n\t\t(record.decodeTokensPerSecond === undefined || isPositiveFiniteNumber(record.decodeTokensPerSecond)) &&\n\t\t(record.loadMs === undefined || isPositiveFiniteNumber(record.loadMs)) &&\n\t\ttypeof samples === \"number\" &&\n\t\tNumber.isInteger(samples) &&\n\t\tsamples >= 0 &&\n\t\ttypeof record.updatedAt === \"string\"\n\t);\n}\n\nexport function hasUsableModelPerfSample(sample: ModelPerfSample): boolean {\n\treturn (\n\t\tprefillRateFromSample(sample) !== undefined ||\n\t\tdecodeRateFromSample(sample) !== undefined ||\n\t\tisPositiveFiniteNumber(sample.loadMs)\n\t);\n}\n\nexport function updateModelPerfProfile(\n\tcurrent: ModelPerfProfile | undefined,\n\tsample: ModelPerfSample,\n\tat: string = sample.at ?? new Date().toISOString(),\n): ModelPerfProfile | undefined {\n\tconst prefillRate = prefillRateFromSample(sample);\n\tconst decodeRate = decodeRateFromSample(sample);\n\tconst loadMs = isPositiveFiniteNumber(sample.loadMs) ? sample.loadMs : undefined;\n\tif (prefillRate === undefined && decodeRate === undefined && loadMs === undefined) return current;\n\n\tconst previousSamples = current?.samples ?? 0;\n\treturn {\n\t\t...(prefillRate !== undefined && {\n\t\t\tprefillTokensPerSecond: updateEwma(current?.prefillTokensPerSecond, prefillRate, previousSamples),\n\t\t}),\n\t\t...(prefillRate === undefined && current?.prefillTokensPerSecond !== undefined\n\t\t\t? { prefillTokensPerSecond: current.prefillTokensPerSecond }\n\t\t\t: {}),\n\t\t...(decodeRate !== undefined && {\n\t\t\tdecodeTokensPerSecond: updateEwma(current?.decodeTokensPerSecond, decodeRate, previousSamples),\n\t\t}),\n\t\t...(decodeRate === undefined && current?.decodeTokensPerSecond !== undefined\n\t\t\t? { decodeTokensPerSecond: current.decodeTokensPerSecond }\n\t\t\t: {}),\n\t\t...(loadMs !== undefined && { loadMs: updateEwma(current?.loadMs, loadMs, previousSamples) }),\n\t\t...(loadMs === undefined && current?.loadMs !== undefined ? { loadMs: current.loadMs } : {}),\n\t\tsamples: previousSamples + 1,\n\t\tupdatedAt: at,\n\t};\n}\n\nexport function resolveAdaptiveStreamIdleOptions(input: AdaptiveStreamIdleInput): Partial<StreamIdleOptions> {\n\tconst profile = input.profile;\n\tconst result: Partial<StreamIdleOptions> = {};\n\tconst localConnectDefaultMs = input.localClass\n\t\t? Math.max(input.base.connectMs, input.base.quietIdleMs)\n\t\t: input.base.connectMs;\n\tconst quietCeilingMs = Math.max(input.base.quietIdleMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\tconst connectCeilingMs = Math.max(localConnectDefaultMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\n\tconst expectedPrefillMs = expectedPrefillFromProfile(profile, input.promptTokens);\n\tif (expectedPrefillMs !== undefined) {\n\t\tconst quietIdleMs = adaptiveBound(input.base.quietIdleMs, quietCeilingMs, expectedPrefillMs);\n\t\tif (quietIdleMs > input.base.quietIdleMs) result.quietIdleMs = quietIdleMs;\n\t}\n\n\tif (input.localClass) {\n\t\tconst expectedConnectWorkMs =\n\t\t\texpectedPrefillMs !== undefined ? expectedPrefillMs + (profile?.loadMs ?? 0) : profile?.loadMs;\n\t\tconst connectMs =\n\t\t\texpectedConnectWorkMs !== undefined\n\t\t\t\t? adaptiveBound(localConnectDefaultMs, connectCeilingMs, expectedConnectWorkMs)\n\t\t\t\t: localConnectDefaultMs;\n\t\tif (connectMs > input.base.connectMs) result.connectMs = connectMs;\n\t}\n\n\treturn result;\n}\n\nexport function estimateContextPromptTokens(context: Context): number {\n\tconst serialized = JSON.stringify({\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: context.messages,\n\t\ttools: context.tools,\n\t});\n\tif (!serialized) return 0;\n\treturn Math.ceil(serialized.length / CHARS_PER_TOKEN_ESTIMATE);\n}\n\nexport function withModelPerfProfile(streamFn: StreamFn, recorder: ModelPerfProfileStreamRecorder): StreamFn {\n\treturn async (model, context, streamOptions) => {\n\t\tconst nowMs = recorder.nowMs ?? Date.now;\n\t\tconst nowIso = recorder.nowIso ?? (() => new Date().toISOString());\n\t\tconst requestStartedAtMs = nowMs();\n\t\tlet responseHeadersAtMs: number | undefined;\n\t\tlet firstTokenAtMs: number | undefined;\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst inner = await streamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\tresponseHeadersAtMs ??= nowMs();\n\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t},\n\t\t});\n\t\tconst outer = createAssistantMessageEventStream();\n\n\t\tvoid (async () => {\n\t\t\tlet terminalPushed = false;\n\t\t\tfor await (const event of inner) {\n\t\t\t\tif (firstTokenAtMs === undefined && isFirstTokenEvent(event)) {\n\t\t\t\t\tfirstTokenAtMs = nowMs();\n\t\t\t\t}\n\t\t\t\tif (event.type === \"done\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\trecordSuccessfulStreamSample({\n\t\t\t\t\t\tcompletionTokens: event.message.usage.output,\n\t\t\t\t\t\tdoneAtMs: nowMs(),\n\t\t\t\t\t\tfirstTokenAtMs,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tnowIso,\n\t\t\t\t\t\tpromptTokens: event.message.usage.input,\n\t\t\t\t\t\trecorder,\n\t\t\t\t\t\trequestStartedAtMs,\n\t\t\t\t\t\tresponseHeadersAtMs,\n\t\t\t\t\t});\n\t\t\t\t} else if (event.type === \"error\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t}\n\t\t\t\touter.push(event);\n\t\t\t\tif (terminalPushed) return;\n\t\t\t}\n\t\t\touter.end();\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n\nfunction recordSuccessfulStreamSample(input: {\n\tmodel: Model<Api>;\n\trecorder: ModelPerfProfileStreamRecorder;\n\trequestStartedAtMs: number;\n\tresponseHeadersAtMs: number | undefined;\n\tfirstTokenAtMs: number | undefined;\n\tdoneAtMs: number;\n\tpromptTokens: number;\n\tcompletionTokens: number;\n\tnowIso: () => string;\n}): void {\n\tif (input.firstTokenAtMs === undefined) return;\n\tconst modelKey = input.recorder.modelKey(input.model);\n\tif (!modelKey) return;\n\tconst sample: ModelPerfSample = {\n\t\tpromptTokens: input.promptTokens,\n\t\tcompletionTokens: input.completionTokens,\n\t\trequestToFirstTokenMs: input.firstTokenAtMs - input.requestStartedAtMs,\n\t\tfirstTokenToDoneMs: input.doneAtMs - input.firstTokenAtMs,\n\t\tat: input.nowIso(),\n\t};\n\tif (input.responseHeadersAtMs !== undefined) {\n\t\tsample.headersToFirstTokenMs = input.firstTokenAtMs - input.responseHeadersAtMs;\n\t}\n\tif (!hasUsableModelPerfSample(sample)) return;\n\ttry {\n\t\tinput.recorder.recordSample(modelKey, sample);\n\t} catch {\n\t\t// Perf profiling must never fail the user turn.\n\t}\n}\n\nfunction isFirstTokenEvent(event: AssistantMessageEvent): boolean {\n\treturn (\n\t\t(event.type === \"text_delta\" || event.type === \"thinking_delta\" || event.type === \"toolcall_delta\") &&\n\t\tevent.delta.length > 0\n\t);\n}\n\nfunction expectedPrefillFromProfile(profile: ModelPerfProfile | undefined, promptTokens: number): number | undefined {\n\tconst prefillRate = profile?.prefillTokensPerSecond;\n\tif (!profile || profile.samples < 1 || !prefillRate || promptTokens <= 0) return undefined;\n\tconst expectedPrefillMs = (promptTokens / prefillRate) * 1000;\n\treturn Number.isFinite(expectedPrefillMs) && expectedPrefillMs > 0 ? expectedPrefillMs : undefined;\n}\n\nfunction adaptiveBound(defaultMs: number, ceilingMs: number, expectedMs: number): number {\n\treturn Math.min(Math.ceil(Math.max(defaultMs, expectedMs * 3)), ceilingMs);\n}\n\nfunction prefillRateFromSample(sample: ModelPerfSample): number | undefined {\n\tconst durationMs = isPositiveFiniteNumber(sample.headersToFirstTokenMs)\n\t\t? sample.headersToFirstTokenMs\n\t\t: sample.requestToFirstTokenMs;\n\treturn tokensPerSecond(sample.promptTokens, durationMs);\n}\n\nfunction decodeRateFromSample(sample: ModelPerfSample): number | undefined {\n\treturn tokensPerSecond(sample.completionTokens, sample.firstTokenToDoneMs);\n}\n\nfunction tokensPerSecond(tokens: number | undefined, durationMs: number | undefined): number | undefined {\n\tif (!isPositiveFiniteNumber(tokens) || !isPositiveFiniteNumber(durationMs)) return undefined;\n\treturn tokens / (durationMs / 1000);\n}\n\nfunction updateEwma(previous: number | undefined, next: number, previousSamples: number): number {\n\tif (previous === undefined || previousSamples === 0) return next;\n\treturn previous * (1 - PERF_EWMA_ALPHA) + next * PERF_EWMA_ALPHA;\n}\n\nfunction isPositiveFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"perf-profile.d.ts","sourceRoot":"","sources":["../../../src/core/models/perf-profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAC7E,OAAO,EACN,KAAK,GAAG,EAGR,KAAK,OAAO,EAEZ,KAAK,KAAK,EAEV,MAAM,mBAAmB,CAAC;AAM3B,eAAO,MAAM,uCAAuC,QAAiB,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAChC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,uBAAuB;IACvC,IAAI,EAAE,iBAAiB,CAAC;IACxB,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,8BAA8B;IAC9C,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,MAAM,GAAG,SAAS,CAAC;IACpD,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,KAAK,IAAI,CAAC;IAClE,KAAK,CAAC,EAAE,MAAM,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,MAAM,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAa5E;AAED,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAMzE;AAED,wBAAgB,sBAAsB,CACrC,OAAO,EAAE,gBAAgB,GAAG,SAAS,EACrC,MAAM,EAAE,eAAe,EACvB,EAAE,GAAE,MAA8C,GAChD,gBAAgB,GAAG,SAAS,CAyB9B;AAED,wBAAgB,gCAAgC,CAAC,KAAK,EAAE,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0B3G;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAQpE;AAED,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,EAAE,8BAA8B,GAAG,QAAQ,CA4D3G","sourcesContent":["import type { StreamFn, StreamIdleOptions } from \"@caupulican/pi-agent-core\";\nimport {\n\ttype Api,\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\n\nconst PERF_EWMA_ALPHA = 0.3;\nconst CHARS_PER_TOKEN_ESTIMATE = 4;\nconst DEFERRED_HEADERS_MAX_GAP_MS = 100;\nconst DEFERRED_HEADERS_MIN_REQUEST_MS = 1_000;\nexport const DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS = 30 * 60 * 1000;\n\nexport interface ModelPerfProfile {\n\tprefillTokensPerSecond?: number;\n\tdecodeTokensPerSecond?: number;\n\tloadMs?: number;\n\tsamples: number;\n\tupdatedAt: string;\n}\n\nexport interface ModelPerfSample {\n\tpromptTokens?: number;\n\tcompletionTokens?: number;\n\theadersToFirstTokenMs?: number;\n\trequestToFirstTokenMs?: number;\n\tfirstTokenToDoneMs?: number;\n\tloadMs?: number;\n\tat?: string;\n}\n\nexport interface AdaptiveStreamIdleInput {\n\tbase: StreamIdleOptions;\n\tprofile?: ModelPerfProfile;\n\tpromptTokens: number;\n\tceilingMs?: number;\n\tlocalClass?: boolean;\n}\n\nexport interface ModelPerfProfileStreamRecorder {\n\tmodelKey: (model: Model<Api>) => string | undefined;\n\trecordSample: (modelKey: string, sample: ModelPerfSample) => void;\n\tnowMs?: () => number;\n\tnowIso?: () => string;\n}\n\nexport function isModelPerfProfile(value: unknown): value is ModelPerfProfile {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst record = value as Record<string, unknown>;\n\tconst samples = record.samples;\n\treturn (\n\t\t(record.prefillTokensPerSecond === undefined || isPositiveFiniteNumber(record.prefillTokensPerSecond)) &&\n\t\t(record.decodeTokensPerSecond === undefined || isPositiveFiniteNumber(record.decodeTokensPerSecond)) &&\n\t\t(record.loadMs === undefined || isPositiveFiniteNumber(record.loadMs)) &&\n\t\ttypeof samples === \"number\" &&\n\t\tNumber.isInteger(samples) &&\n\t\tsamples >= 0 &&\n\t\ttypeof record.updatedAt === \"string\"\n\t);\n}\n\nexport function hasUsableModelPerfSample(sample: ModelPerfSample): boolean {\n\treturn (\n\t\tprefillRateFromSample(sample) !== undefined ||\n\t\tdecodeRateFromSample(sample) !== undefined ||\n\t\tisPositiveFiniteNumber(sample.loadMs)\n\t);\n}\n\nexport function updateModelPerfProfile(\n\tcurrent: ModelPerfProfile | undefined,\n\tsample: ModelPerfSample,\n\tat: string = sample.at ?? new Date().toISOString(),\n): ModelPerfProfile | undefined {\n\tconst prefillRate = prefillRateFromSample(sample);\n\tconst decodeRate = decodeRateFromSample(sample);\n\tconst loadMs = isPositiveFiniteNumber(sample.loadMs) ? sample.loadMs : undefined;\n\tif (prefillRate === undefined && decodeRate === undefined && loadMs === undefined) return current;\n\n\tconst previousSamples = current?.samples ?? 0;\n\treturn {\n\t\t...(prefillRate !== undefined && {\n\t\t\tprefillTokensPerSecond: updateEwma(current?.prefillTokensPerSecond, prefillRate, previousSamples),\n\t\t}),\n\t\t...(prefillRate === undefined && current?.prefillTokensPerSecond !== undefined\n\t\t\t? { prefillTokensPerSecond: current.prefillTokensPerSecond }\n\t\t\t: {}),\n\t\t...(decodeRate !== undefined && {\n\t\t\tdecodeTokensPerSecond: updateEwma(current?.decodeTokensPerSecond, decodeRate, previousSamples),\n\t\t}),\n\t\t...(decodeRate === undefined && current?.decodeTokensPerSecond !== undefined\n\t\t\t? { decodeTokensPerSecond: current.decodeTokensPerSecond }\n\t\t\t: {}),\n\t\t...(loadMs !== undefined && { loadMs: updateEwma(current?.loadMs, loadMs, previousSamples) }),\n\t\t...(loadMs === undefined && current?.loadMs !== undefined ? { loadMs: current.loadMs } : {}),\n\t\tsamples: previousSamples + 1,\n\t\tupdatedAt: at,\n\t};\n}\n\nexport function resolveAdaptiveStreamIdleOptions(input: AdaptiveStreamIdleInput): Partial<StreamIdleOptions> {\n\tconst profile = input.profile;\n\tconst result: Partial<StreamIdleOptions> = {};\n\tconst localConnectDefaultMs = input.localClass\n\t\t? Math.max(input.base.connectMs, input.base.quietIdleMs)\n\t\t: input.base.connectMs;\n\tconst quietCeilingMs = Math.max(input.base.quietIdleMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\tconst connectCeilingMs = Math.max(localConnectDefaultMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\n\tconst expectedPrefillMs = expectedPrefillFromProfile(profile, input.promptTokens);\n\tif (expectedPrefillMs !== undefined) {\n\t\tconst quietIdleMs = adaptiveBound(input.base.quietIdleMs, quietCeilingMs, expectedPrefillMs);\n\t\tif (quietIdleMs > input.base.quietIdleMs) result.quietIdleMs = quietIdleMs;\n\t}\n\n\tif (input.localClass) {\n\t\tconst expectedConnectWorkMs =\n\t\t\texpectedPrefillMs !== undefined ? expectedPrefillMs + (profile?.loadMs ?? 0) : profile?.loadMs;\n\t\tconst connectMs =\n\t\t\texpectedConnectWorkMs !== undefined\n\t\t\t\t? adaptiveBound(localConnectDefaultMs, connectCeilingMs, expectedConnectWorkMs)\n\t\t\t\t: localConnectDefaultMs;\n\t\tif (connectMs > input.base.connectMs) result.connectMs = connectMs;\n\t}\n\n\treturn result;\n}\n\nexport function estimateContextPromptTokens(context: Context): number {\n\tconst serialized = JSON.stringify({\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: context.messages,\n\t\ttools: context.tools,\n\t});\n\tif (!serialized) return 0;\n\treturn Math.ceil(serialized.length / CHARS_PER_TOKEN_ESTIMATE);\n}\n\nexport function withModelPerfProfile(streamFn: StreamFn, recorder: ModelPerfProfileStreamRecorder): StreamFn {\n\treturn async (model, context, streamOptions) => {\n\t\tconst nowMs = recorder.nowMs ?? Date.now;\n\t\tconst nowIso = recorder.nowIso ?? (() => new Date().toISOString());\n\t\tconst requestStartedAtMs = nowMs();\n\t\tlet responseHeadersAtMs: number | undefined;\n\t\tlet firstTokenAtMs: number | undefined;\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst outer = createAssistantMessageEventStream();\n\t\tlet latest = emptyAssistantMessage(model);\n\t\tlet inner: Awaited<ReturnType<StreamFn>>;\n\t\ttry {\n\t\t\tinner = await streamFn(model, context, {\n\t\t\t\t...streamOptions,\n\t\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\t\tresponseHeadersAtMs ??= nowMs();\n\t\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\touter.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true, error));\n\t\t\treturn outer;\n\t\t}\n\n\t\tvoid (async () => {\n\t\t\tlet terminalPushed = false;\n\t\t\ttry {\n\t\t\t\tfor await (const event of inner) {\n\t\t\t\t\tlatest = assistantMessageFromEvent(event);\n\t\t\t\t\tif (firstTokenAtMs === undefined && isFirstTokenEvent(event)) {\n\t\t\t\t\t\tfirstTokenAtMs = nowMs();\n\t\t\t\t\t}\n\t\t\t\t\tif (event.type === \"done\") {\n\t\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\t\trecordSuccessfulStreamSample({\n\t\t\t\t\t\t\tcompletionTokens: event.message.usage.output,\n\t\t\t\t\t\t\tdoneAtMs: nowMs(),\n\t\t\t\t\t\t\tfirstTokenAtMs,\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\tnowIso,\n\t\t\t\t\t\t\tpromptTokens:\n\t\t\t\t\t\t\t\tevent.message.usage.input + event.message.usage.cacheRead + event.message.usage.cacheWrite,\n\t\t\t\t\t\t\trecorder,\n\t\t\t\t\t\t\trequestStartedAtMs,\n\t\t\t\t\t\t\tresponseHeadersAtMs,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (event.type === \"error\") {\n\t\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\t}\n\t\t\t\t\touter.push(event);\n\t\t\t\t\tif (terminalPushed) return;\n\t\t\t\t}\n\t\t\t\touter.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true));\n\t\t\t} catch (error) {\n\t\t\t\touter.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true, error));\n\t\t\t}\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n\nfunction assistantMessageFromEvent(event: AssistantMessageEvent): AssistantMessage {\n\tif (event.type === \"done\") return event.message;\n\tif (event.type === \"error\") return event.error;\n\treturn event.partial;\n}\n\nfunction emptyAssistantMessage(model: Model<Api>): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: \"stop\",\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nfunction perfStreamFailure(latest: AssistantMessage, aborted: boolean, error?: unknown): AssistantMessageEvent {\n\tconst stopReason = aborted ? \"aborted\" : \"error\";\n\tconst detail = error instanceof Error ? `: ${error.message}` : error === undefined ? \"\" : `: ${String(error)}`;\n\treturn {\n\t\ttype: \"error\",\n\t\treason: stopReason,\n\t\terror: {\n\t\t\t...latest,\n\t\t\tstopReason,\n\t\t\terrorMessage: aborted\n\t\t\t\t? `stream aborted before terminal event${detail}`\n\t\t\t\t: `stream ended without terminal event${detail}`,\n\t\t},\n\t};\n}\n\nfunction recordSuccessfulStreamSample(input: {\n\tmodel: Model<Api>;\n\trecorder: ModelPerfProfileStreamRecorder;\n\trequestStartedAtMs: number;\n\tresponseHeadersAtMs: number | undefined;\n\tfirstTokenAtMs: number | undefined;\n\tdoneAtMs: number;\n\tpromptTokens: number;\n\tcompletionTokens: number;\n\tnowIso: () => string;\n}): void {\n\tif (input.firstTokenAtMs === undefined) return;\n\tconst modelKey = input.recorder.modelKey(input.model);\n\tif (!modelKey) return;\n\tconst requestToFirstTokenMs = input.firstTokenAtMs - input.requestStartedAtMs;\n\tconst sample: ModelPerfSample = {\n\t\tpromptTokens: input.promptTokens,\n\t\tcompletionTokens: input.completionTokens,\n\t\trequestToFirstTokenMs,\n\t\tfirstTokenToDoneMs: input.doneAtMs - input.firstTokenAtMs,\n\t\tat: input.nowIso(),\n\t};\n\tif (input.responseHeadersAtMs !== undefined) {\n\t\tconst headersToFirstTokenMs = input.firstTokenAtMs - input.responseHeadersAtMs;\n\t\tconst headersWereEffectivelyDeferred =\n\t\t\trequestToFirstTokenMs >= DEFERRED_HEADERS_MIN_REQUEST_MS &&\n\t\t\theadersToFirstTokenMs <= DEFERRED_HEADERS_MAX_GAP_MS;\n\t\tif (!headersWereEffectivelyDeferred) sample.headersToFirstTokenMs = headersToFirstTokenMs;\n\t}\n\tif (!hasUsableModelPerfSample(sample)) return;\n\ttry {\n\t\tinput.recorder.recordSample(modelKey, sample);\n\t} catch {\n\t\t// Perf profiling must never fail the user turn.\n\t}\n}\n\nfunction isFirstTokenEvent(event: AssistantMessageEvent): boolean {\n\treturn (\n\t\t(event.type === \"text_delta\" || event.type === \"thinking_delta\" || event.type === \"toolcall_delta\") &&\n\t\tevent.delta.length > 0\n\t);\n}\n\nfunction expectedPrefillFromProfile(profile: ModelPerfProfile | undefined, promptTokens: number): number | undefined {\n\tconst prefillRate = profile?.prefillTokensPerSecond;\n\tif (!profile || profile.samples < 1 || !prefillRate || promptTokens <= 0) return undefined;\n\tconst expectedPrefillMs = (promptTokens / prefillRate) * 1000;\n\treturn Number.isFinite(expectedPrefillMs) && expectedPrefillMs > 0 ? expectedPrefillMs : undefined;\n}\n\nfunction adaptiveBound(defaultMs: number, ceilingMs: number, expectedMs: number): number {\n\treturn Math.min(Math.ceil(Math.max(defaultMs, expectedMs * 3)), ceilingMs);\n}\n\nfunction prefillRateFromSample(sample: ModelPerfSample): number | undefined {\n\tconst durationMs = isPositiveFiniteNumber(sample.headersToFirstTokenMs)\n\t\t? sample.headersToFirstTokenMs\n\t\t: sample.requestToFirstTokenMs;\n\treturn tokensPerSecond(sample.promptTokens, durationMs);\n}\n\nfunction decodeRateFromSample(sample: ModelPerfSample): number | undefined {\n\treturn tokensPerSecond(sample.completionTokens, sample.firstTokenToDoneMs);\n}\n\nfunction tokensPerSecond(tokens: number | undefined, durationMs: number | undefined): number | undefined {\n\tif (!isPositiveFiniteNumber(tokens) || !isPositiveFiniteNumber(durationMs)) return undefined;\n\treturn tokens / (durationMs / 1000);\n}\n\nfunction updateEwma(previous: number | undefined, next: number, previousSamples: number): number {\n\tif (previous === undefined || previousSamples === 0) return next;\n\treturn previous * (1 - PERF_EWMA_ALPHA) + next * PERF_EWMA_ALPHA;\n}\n\nfunction isPositiveFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n"]}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "@caupulican/pi-ai";
|
|
2
2
|
const PERF_EWMA_ALPHA = 0.3;
|
|
3
3
|
const CHARS_PER_TOKEN_ESTIMATE = 4;
|
|
4
|
+
const DEFERRED_HEADERS_MAX_GAP_MS = 100;
|
|
5
|
+
const DEFERRED_HEADERS_MIN_REQUEST_MS = 1_000;
|
|
4
6
|
export const DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS = 30 * 60 * 1000;
|
|
5
7
|
export function isModelPerfProfile(value) {
|
|
6
8
|
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
@@ -88,61 +90,121 @@ export function withModelPerfProfile(streamFn, recorder) {
|
|
|
88
90
|
let responseHeadersAtMs;
|
|
89
91
|
let firstTokenAtMs;
|
|
90
92
|
const originalOnResponse = streamOptions?.onResponse;
|
|
91
|
-
const inner = await streamFn(model, context, {
|
|
92
|
-
...streamOptions,
|
|
93
|
-
onResponse: async (response, responseModel) => {
|
|
94
|
-
responseHeadersAtMs ??= nowMs();
|
|
95
|
-
await originalOnResponse?.(response, responseModel);
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
93
|
const outer = createAssistantMessageEventStream();
|
|
94
|
+
let latest = emptyAssistantMessage(model);
|
|
95
|
+
let inner;
|
|
96
|
+
try {
|
|
97
|
+
inner = await streamFn(model, context, {
|
|
98
|
+
...streamOptions,
|
|
99
|
+
onResponse: async (response, responseModel) => {
|
|
100
|
+
responseHeadersAtMs ??= nowMs();
|
|
101
|
+
await originalOnResponse?.(response, responseModel);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
outer.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true, error));
|
|
107
|
+
return outer;
|
|
108
|
+
}
|
|
99
109
|
void (async () => {
|
|
100
110
|
let terminalPushed = false;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
try {
|
|
112
|
+
for await (const event of inner) {
|
|
113
|
+
latest = assistantMessageFromEvent(event);
|
|
114
|
+
if (firstTokenAtMs === undefined && isFirstTokenEvent(event)) {
|
|
115
|
+
firstTokenAtMs = nowMs();
|
|
116
|
+
}
|
|
117
|
+
if (event.type === "done") {
|
|
118
|
+
terminalPushed = true;
|
|
119
|
+
recordSuccessfulStreamSample({
|
|
120
|
+
completionTokens: event.message.usage.output,
|
|
121
|
+
doneAtMs: nowMs(),
|
|
122
|
+
firstTokenAtMs,
|
|
123
|
+
model,
|
|
124
|
+
nowIso,
|
|
125
|
+
promptTokens: event.message.usage.input + event.message.usage.cacheRead + event.message.usage.cacheWrite,
|
|
126
|
+
recorder,
|
|
127
|
+
requestStartedAtMs,
|
|
128
|
+
responseHeadersAtMs,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
else if (event.type === "error") {
|
|
132
|
+
terminalPushed = true;
|
|
133
|
+
}
|
|
134
|
+
outer.push(event);
|
|
135
|
+
if (terminalPushed)
|
|
136
|
+
return;
|
|
104
137
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
doneAtMs: nowMs(),
|
|
110
|
-
firstTokenAtMs,
|
|
111
|
-
model,
|
|
112
|
-
nowIso,
|
|
113
|
-
promptTokens: event.message.usage.input,
|
|
114
|
-
recorder,
|
|
115
|
-
requestStartedAtMs,
|
|
116
|
-
responseHeadersAtMs,
|
|
117
|
-
});
|
|
118
|
-
}
|
|
119
|
-
else if (event.type === "error") {
|
|
120
|
-
terminalPushed = true;
|
|
121
|
-
}
|
|
122
|
-
outer.push(event);
|
|
123
|
-
if (terminalPushed)
|
|
124
|
-
return;
|
|
138
|
+
outer.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
outer.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true, error));
|
|
125
142
|
}
|
|
126
|
-
outer.end();
|
|
127
143
|
})();
|
|
128
144
|
return outer;
|
|
129
145
|
};
|
|
130
146
|
}
|
|
147
|
+
function assistantMessageFromEvent(event) {
|
|
148
|
+
if (event.type === "done")
|
|
149
|
+
return event.message;
|
|
150
|
+
if (event.type === "error")
|
|
151
|
+
return event.error;
|
|
152
|
+
return event.partial;
|
|
153
|
+
}
|
|
154
|
+
function emptyAssistantMessage(model) {
|
|
155
|
+
return {
|
|
156
|
+
role: "assistant",
|
|
157
|
+
content: [],
|
|
158
|
+
api: model.api,
|
|
159
|
+
provider: model.provider,
|
|
160
|
+
model: model.id,
|
|
161
|
+
usage: {
|
|
162
|
+
input: 0,
|
|
163
|
+
output: 0,
|
|
164
|
+
cacheRead: 0,
|
|
165
|
+
cacheWrite: 0,
|
|
166
|
+
totalTokens: 0,
|
|
167
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
168
|
+
},
|
|
169
|
+
stopReason: "stop",
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function perfStreamFailure(latest, aborted, error) {
|
|
174
|
+
const stopReason = aborted ? "aborted" : "error";
|
|
175
|
+
const detail = error instanceof Error ? `: ${error.message}` : error === undefined ? "" : `: ${String(error)}`;
|
|
176
|
+
return {
|
|
177
|
+
type: "error",
|
|
178
|
+
reason: stopReason,
|
|
179
|
+
error: {
|
|
180
|
+
...latest,
|
|
181
|
+
stopReason,
|
|
182
|
+
errorMessage: aborted
|
|
183
|
+
? `stream aborted before terminal event${detail}`
|
|
184
|
+
: `stream ended without terminal event${detail}`,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
131
188
|
function recordSuccessfulStreamSample(input) {
|
|
132
189
|
if (input.firstTokenAtMs === undefined)
|
|
133
190
|
return;
|
|
134
191
|
const modelKey = input.recorder.modelKey(input.model);
|
|
135
192
|
if (!modelKey)
|
|
136
193
|
return;
|
|
194
|
+
const requestToFirstTokenMs = input.firstTokenAtMs - input.requestStartedAtMs;
|
|
137
195
|
const sample = {
|
|
138
196
|
promptTokens: input.promptTokens,
|
|
139
197
|
completionTokens: input.completionTokens,
|
|
140
|
-
requestToFirstTokenMs
|
|
198
|
+
requestToFirstTokenMs,
|
|
141
199
|
firstTokenToDoneMs: input.doneAtMs - input.firstTokenAtMs,
|
|
142
200
|
at: input.nowIso(),
|
|
143
201
|
};
|
|
144
202
|
if (input.responseHeadersAtMs !== undefined) {
|
|
145
|
-
|
|
203
|
+
const headersToFirstTokenMs = input.firstTokenAtMs - input.responseHeadersAtMs;
|
|
204
|
+
const headersWereEffectivelyDeferred = requestToFirstTokenMs >= DEFERRED_HEADERS_MIN_REQUEST_MS &&
|
|
205
|
+
headersToFirstTokenMs <= DEFERRED_HEADERS_MAX_GAP_MS;
|
|
206
|
+
if (!headersWereEffectivelyDeferred)
|
|
207
|
+
sample.headersToFirstTokenMs = headersToFirstTokenMs;
|
|
146
208
|
}
|
|
147
209
|
if (!hasUsableModelPerfSample(sample))
|
|
148
210
|
return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"perf-profile.js","sourceRoot":"","sources":["../../../src/core/models/perf-profile.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StreamFn, StreamIdleOptions } from \"@caupulican/pi-agent-core\";\nimport {\n\ttype Api,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\n\nconst PERF_EWMA_ALPHA = 0.3;\nconst CHARS_PER_TOKEN_ESTIMATE = 4;\nexport const DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS = 30 * 60 * 1000;\n\nexport interface ModelPerfProfile {\n\tprefillTokensPerSecond?: number;\n\tdecodeTokensPerSecond?: number;\n\tloadMs?: number;\n\tsamples: number;\n\tupdatedAt: string;\n}\n\nexport interface ModelPerfSample {\n\tpromptTokens?: number;\n\tcompletionTokens?: number;\n\theadersToFirstTokenMs?: number;\n\trequestToFirstTokenMs?: number;\n\tfirstTokenToDoneMs?: number;\n\tloadMs?: number;\n\tat?: string;\n}\n\nexport interface AdaptiveStreamIdleInput {\n\tbase: StreamIdleOptions;\n\tprofile?: ModelPerfProfile;\n\tpromptTokens: number;\n\tceilingMs?: number;\n\tlocalClass?: boolean;\n}\n\nexport interface ModelPerfProfileStreamRecorder {\n\tmodelKey: (model: Model<Api>) => string | undefined;\n\trecordSample: (modelKey: string, sample: ModelPerfSample) => void;\n\tnowMs?: () => number;\n\tnowIso?: () => string;\n}\n\nexport function isModelPerfProfile(value: unknown): value is ModelPerfProfile {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst record = value as Record<string, unknown>;\n\tconst samples = record.samples;\n\treturn (\n\t\t(record.prefillTokensPerSecond === undefined || isPositiveFiniteNumber(record.prefillTokensPerSecond)) &&\n\t\t(record.decodeTokensPerSecond === undefined || isPositiveFiniteNumber(record.decodeTokensPerSecond)) &&\n\t\t(record.loadMs === undefined || isPositiveFiniteNumber(record.loadMs)) &&\n\t\ttypeof samples === \"number\" &&\n\t\tNumber.isInteger(samples) &&\n\t\tsamples >= 0 &&\n\t\ttypeof record.updatedAt === \"string\"\n\t);\n}\n\nexport function hasUsableModelPerfSample(sample: ModelPerfSample): boolean {\n\treturn (\n\t\tprefillRateFromSample(sample) !== undefined ||\n\t\tdecodeRateFromSample(sample) !== undefined ||\n\t\tisPositiveFiniteNumber(sample.loadMs)\n\t);\n}\n\nexport function updateModelPerfProfile(\n\tcurrent: ModelPerfProfile | undefined,\n\tsample: ModelPerfSample,\n\tat: string = sample.at ?? new Date().toISOString(),\n): ModelPerfProfile | undefined {\n\tconst prefillRate = prefillRateFromSample(sample);\n\tconst decodeRate = decodeRateFromSample(sample);\n\tconst loadMs = isPositiveFiniteNumber(sample.loadMs) ? sample.loadMs : undefined;\n\tif (prefillRate === undefined && decodeRate === undefined && loadMs === undefined) return current;\n\n\tconst previousSamples = current?.samples ?? 0;\n\treturn {\n\t\t...(prefillRate !== undefined && {\n\t\t\tprefillTokensPerSecond: updateEwma(current?.prefillTokensPerSecond, prefillRate, previousSamples),\n\t\t}),\n\t\t...(prefillRate === undefined && current?.prefillTokensPerSecond !== undefined\n\t\t\t? { prefillTokensPerSecond: current.prefillTokensPerSecond }\n\t\t\t: {}),\n\t\t...(decodeRate !== undefined && {\n\t\t\tdecodeTokensPerSecond: updateEwma(current?.decodeTokensPerSecond, decodeRate, previousSamples),\n\t\t}),\n\t\t...(decodeRate === undefined && current?.decodeTokensPerSecond !== undefined\n\t\t\t? { decodeTokensPerSecond: current.decodeTokensPerSecond }\n\t\t\t: {}),\n\t\t...(loadMs !== undefined && { loadMs: updateEwma(current?.loadMs, loadMs, previousSamples) }),\n\t\t...(loadMs === undefined && current?.loadMs !== undefined ? { loadMs: current.loadMs } : {}),\n\t\tsamples: previousSamples + 1,\n\t\tupdatedAt: at,\n\t};\n}\n\nexport function resolveAdaptiveStreamIdleOptions(input: AdaptiveStreamIdleInput): Partial<StreamIdleOptions> {\n\tconst profile = input.profile;\n\tconst result: Partial<StreamIdleOptions> = {};\n\tconst localConnectDefaultMs = input.localClass\n\t\t? Math.max(input.base.connectMs, input.base.quietIdleMs)\n\t\t: input.base.connectMs;\n\tconst quietCeilingMs = Math.max(input.base.quietIdleMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\tconst connectCeilingMs = Math.max(localConnectDefaultMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\n\tconst expectedPrefillMs = expectedPrefillFromProfile(profile, input.promptTokens);\n\tif (expectedPrefillMs !== undefined) {\n\t\tconst quietIdleMs = adaptiveBound(input.base.quietIdleMs, quietCeilingMs, expectedPrefillMs);\n\t\tif (quietIdleMs > input.base.quietIdleMs) result.quietIdleMs = quietIdleMs;\n\t}\n\n\tif (input.localClass) {\n\t\tconst expectedConnectWorkMs =\n\t\t\texpectedPrefillMs !== undefined ? expectedPrefillMs + (profile?.loadMs ?? 0) : profile?.loadMs;\n\t\tconst connectMs =\n\t\t\texpectedConnectWorkMs !== undefined\n\t\t\t\t? adaptiveBound(localConnectDefaultMs, connectCeilingMs, expectedConnectWorkMs)\n\t\t\t\t: localConnectDefaultMs;\n\t\tif (connectMs > input.base.connectMs) result.connectMs = connectMs;\n\t}\n\n\treturn result;\n}\n\nexport function estimateContextPromptTokens(context: Context): number {\n\tconst serialized = JSON.stringify({\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: context.messages,\n\t\ttools: context.tools,\n\t});\n\tif (!serialized) return 0;\n\treturn Math.ceil(serialized.length / CHARS_PER_TOKEN_ESTIMATE);\n}\n\nexport function withModelPerfProfile(streamFn: StreamFn, recorder: ModelPerfProfileStreamRecorder): StreamFn {\n\treturn async (model, context, streamOptions) => {\n\t\tconst nowMs = recorder.nowMs ?? Date.now;\n\t\tconst nowIso = recorder.nowIso ?? (() => new Date().toISOString());\n\t\tconst requestStartedAtMs = nowMs();\n\t\tlet responseHeadersAtMs: number | undefined;\n\t\tlet firstTokenAtMs: number | undefined;\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst inner = await streamFn(model, context, {\n\t\t\t...streamOptions,\n\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\tresponseHeadersAtMs ??= nowMs();\n\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t},\n\t\t});\n\t\tconst outer = createAssistantMessageEventStream();\n\n\t\tvoid (async () => {\n\t\t\tlet terminalPushed = false;\n\t\t\tfor await (const event of inner) {\n\t\t\t\tif (firstTokenAtMs === undefined && isFirstTokenEvent(event)) {\n\t\t\t\t\tfirstTokenAtMs = nowMs();\n\t\t\t\t}\n\t\t\t\tif (event.type === \"done\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\trecordSuccessfulStreamSample({\n\t\t\t\t\t\tcompletionTokens: event.message.usage.output,\n\t\t\t\t\t\tdoneAtMs: nowMs(),\n\t\t\t\t\t\tfirstTokenAtMs,\n\t\t\t\t\t\tmodel,\n\t\t\t\t\t\tnowIso,\n\t\t\t\t\t\tpromptTokens: event.message.usage.input,\n\t\t\t\t\t\trecorder,\n\t\t\t\t\t\trequestStartedAtMs,\n\t\t\t\t\t\tresponseHeadersAtMs,\n\t\t\t\t\t});\n\t\t\t\t} else if (event.type === \"error\") {\n\t\t\t\t\tterminalPushed = true;\n\t\t\t\t}\n\t\t\t\touter.push(event);\n\t\t\t\tif (terminalPushed) return;\n\t\t\t}\n\t\t\touter.end();\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n\nfunction recordSuccessfulStreamSample(input: {\n\tmodel: Model<Api>;\n\trecorder: ModelPerfProfileStreamRecorder;\n\trequestStartedAtMs: number;\n\tresponseHeadersAtMs: number | undefined;\n\tfirstTokenAtMs: number | undefined;\n\tdoneAtMs: number;\n\tpromptTokens: number;\n\tcompletionTokens: number;\n\tnowIso: () => string;\n}): void {\n\tif (input.firstTokenAtMs === undefined) return;\n\tconst modelKey = input.recorder.modelKey(input.model);\n\tif (!modelKey) return;\n\tconst sample: ModelPerfSample = {\n\t\tpromptTokens: input.promptTokens,\n\t\tcompletionTokens: input.completionTokens,\n\t\trequestToFirstTokenMs: input.firstTokenAtMs - input.requestStartedAtMs,\n\t\tfirstTokenToDoneMs: input.doneAtMs - input.firstTokenAtMs,\n\t\tat: input.nowIso(),\n\t};\n\tif (input.responseHeadersAtMs !== undefined) {\n\t\tsample.headersToFirstTokenMs = input.firstTokenAtMs - input.responseHeadersAtMs;\n\t}\n\tif (!hasUsableModelPerfSample(sample)) return;\n\ttry {\n\t\tinput.recorder.recordSample(modelKey, sample);\n\t} catch {\n\t\t// Perf profiling must never fail the user turn.\n\t}\n}\n\nfunction isFirstTokenEvent(event: AssistantMessageEvent): boolean {\n\treturn (\n\t\t(event.type === \"text_delta\" || event.type === \"thinking_delta\" || event.type === \"toolcall_delta\") &&\n\t\tevent.delta.length > 0\n\t);\n}\n\nfunction expectedPrefillFromProfile(profile: ModelPerfProfile | undefined, promptTokens: number): number | undefined {\n\tconst prefillRate = profile?.prefillTokensPerSecond;\n\tif (!profile || profile.samples < 1 || !prefillRate || promptTokens <= 0) return undefined;\n\tconst expectedPrefillMs = (promptTokens / prefillRate) * 1000;\n\treturn Number.isFinite(expectedPrefillMs) && expectedPrefillMs > 0 ? expectedPrefillMs : undefined;\n}\n\nfunction adaptiveBound(defaultMs: number, ceilingMs: number, expectedMs: number): number {\n\treturn Math.min(Math.ceil(Math.max(defaultMs, expectedMs * 3)), ceilingMs);\n}\n\nfunction prefillRateFromSample(sample: ModelPerfSample): number | undefined {\n\tconst durationMs = isPositiveFiniteNumber(sample.headersToFirstTokenMs)\n\t\t? sample.headersToFirstTokenMs\n\t\t: sample.requestToFirstTokenMs;\n\treturn tokensPerSecond(sample.promptTokens, durationMs);\n}\n\nfunction decodeRateFromSample(sample: ModelPerfSample): number | undefined {\n\treturn tokensPerSecond(sample.completionTokens, sample.firstTokenToDoneMs);\n}\n\nfunction tokensPerSecond(tokens: number | undefined, durationMs: number | undefined): number | undefined {\n\tif (!isPositiveFiniteNumber(tokens) || !isPositiveFiniteNumber(durationMs)) return undefined;\n\treturn tokens / (durationMs / 1000);\n}\n\nfunction updateEwma(previous: number | undefined, next: number, previousSamples: number): number {\n\tif (previous === undefined || previousSamples === 0) return next;\n\treturn previous * (1 - PERF_EWMA_ALPHA) + next * PERF_EWMA_ALPHA;\n}\n\nfunction isPositiveFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"perf-profile.js","sourceRoot":"","sources":["../../../src/core/models/perf-profile.ts"],"names":[],"mappings":"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","sourcesContent":["import type { StreamFn, StreamIdleOptions } from \"@caupulican/pi-agent-core\";\nimport {\n\ttype Api,\n\ttype AssistantMessage,\n\ttype AssistantMessageEvent,\n\ttype Context,\n\tcreateAssistantMessageEventStream,\n\ttype Model,\n\ttype ProviderResponse,\n} from \"@caupulican/pi-ai\";\n\nconst PERF_EWMA_ALPHA = 0.3;\nconst CHARS_PER_TOKEN_ESTIMATE = 4;\nconst DEFERRED_HEADERS_MAX_GAP_MS = 100;\nconst DEFERRED_HEADERS_MIN_REQUEST_MS = 1_000;\nexport const DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS = 30 * 60 * 1000;\n\nexport interface ModelPerfProfile {\n\tprefillTokensPerSecond?: number;\n\tdecodeTokensPerSecond?: number;\n\tloadMs?: number;\n\tsamples: number;\n\tupdatedAt: string;\n}\n\nexport interface ModelPerfSample {\n\tpromptTokens?: number;\n\tcompletionTokens?: number;\n\theadersToFirstTokenMs?: number;\n\trequestToFirstTokenMs?: number;\n\tfirstTokenToDoneMs?: number;\n\tloadMs?: number;\n\tat?: string;\n}\n\nexport interface AdaptiveStreamIdleInput {\n\tbase: StreamIdleOptions;\n\tprofile?: ModelPerfProfile;\n\tpromptTokens: number;\n\tceilingMs?: number;\n\tlocalClass?: boolean;\n}\n\nexport interface ModelPerfProfileStreamRecorder {\n\tmodelKey: (model: Model<Api>) => string | undefined;\n\trecordSample: (modelKey: string, sample: ModelPerfSample) => void;\n\tnowMs?: () => number;\n\tnowIso?: () => string;\n}\n\nexport function isModelPerfProfile(value: unknown): value is ModelPerfProfile {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n\tconst record = value as Record<string, unknown>;\n\tconst samples = record.samples;\n\treturn (\n\t\t(record.prefillTokensPerSecond === undefined || isPositiveFiniteNumber(record.prefillTokensPerSecond)) &&\n\t\t(record.decodeTokensPerSecond === undefined || isPositiveFiniteNumber(record.decodeTokensPerSecond)) &&\n\t\t(record.loadMs === undefined || isPositiveFiniteNumber(record.loadMs)) &&\n\t\ttypeof samples === \"number\" &&\n\t\tNumber.isInteger(samples) &&\n\t\tsamples >= 0 &&\n\t\ttypeof record.updatedAt === \"string\"\n\t);\n}\n\nexport function hasUsableModelPerfSample(sample: ModelPerfSample): boolean {\n\treturn (\n\t\tprefillRateFromSample(sample) !== undefined ||\n\t\tdecodeRateFromSample(sample) !== undefined ||\n\t\tisPositiveFiniteNumber(sample.loadMs)\n\t);\n}\n\nexport function updateModelPerfProfile(\n\tcurrent: ModelPerfProfile | undefined,\n\tsample: ModelPerfSample,\n\tat: string = sample.at ?? new Date().toISOString(),\n): ModelPerfProfile | undefined {\n\tconst prefillRate = prefillRateFromSample(sample);\n\tconst decodeRate = decodeRateFromSample(sample);\n\tconst loadMs = isPositiveFiniteNumber(sample.loadMs) ? sample.loadMs : undefined;\n\tif (prefillRate === undefined && decodeRate === undefined && loadMs === undefined) return current;\n\n\tconst previousSamples = current?.samples ?? 0;\n\treturn {\n\t\t...(prefillRate !== undefined && {\n\t\t\tprefillTokensPerSecond: updateEwma(current?.prefillTokensPerSecond, prefillRate, previousSamples),\n\t\t}),\n\t\t...(prefillRate === undefined && current?.prefillTokensPerSecond !== undefined\n\t\t\t? { prefillTokensPerSecond: current.prefillTokensPerSecond }\n\t\t\t: {}),\n\t\t...(decodeRate !== undefined && {\n\t\t\tdecodeTokensPerSecond: updateEwma(current?.decodeTokensPerSecond, decodeRate, previousSamples),\n\t\t}),\n\t\t...(decodeRate === undefined && current?.decodeTokensPerSecond !== undefined\n\t\t\t? { decodeTokensPerSecond: current.decodeTokensPerSecond }\n\t\t\t: {}),\n\t\t...(loadMs !== undefined && { loadMs: updateEwma(current?.loadMs, loadMs, previousSamples) }),\n\t\t...(loadMs === undefined && current?.loadMs !== undefined ? { loadMs: current.loadMs } : {}),\n\t\tsamples: previousSamples + 1,\n\t\tupdatedAt: at,\n\t};\n}\n\nexport function resolveAdaptiveStreamIdleOptions(input: AdaptiveStreamIdleInput): Partial<StreamIdleOptions> {\n\tconst profile = input.profile;\n\tconst result: Partial<StreamIdleOptions> = {};\n\tconst localConnectDefaultMs = input.localClass\n\t\t? Math.max(input.base.connectMs, input.base.quietIdleMs)\n\t\t: input.base.connectMs;\n\tconst quietCeilingMs = Math.max(input.base.quietIdleMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\tconst connectCeilingMs = Math.max(localConnectDefaultMs, input.ceilingMs ?? DEFAULT_ADAPTIVE_STREAM_IDLE_CEILING_MS);\n\n\tconst expectedPrefillMs = expectedPrefillFromProfile(profile, input.promptTokens);\n\tif (expectedPrefillMs !== undefined) {\n\t\tconst quietIdleMs = adaptiveBound(input.base.quietIdleMs, quietCeilingMs, expectedPrefillMs);\n\t\tif (quietIdleMs > input.base.quietIdleMs) result.quietIdleMs = quietIdleMs;\n\t}\n\n\tif (input.localClass) {\n\t\tconst expectedConnectWorkMs =\n\t\t\texpectedPrefillMs !== undefined ? expectedPrefillMs + (profile?.loadMs ?? 0) : profile?.loadMs;\n\t\tconst connectMs =\n\t\t\texpectedConnectWorkMs !== undefined\n\t\t\t\t? adaptiveBound(localConnectDefaultMs, connectCeilingMs, expectedConnectWorkMs)\n\t\t\t\t: localConnectDefaultMs;\n\t\tif (connectMs > input.base.connectMs) result.connectMs = connectMs;\n\t}\n\n\treturn result;\n}\n\nexport function estimateContextPromptTokens(context: Context): number {\n\tconst serialized = JSON.stringify({\n\t\tsystemPrompt: context.systemPrompt,\n\t\tmessages: context.messages,\n\t\ttools: context.tools,\n\t});\n\tif (!serialized) return 0;\n\treturn Math.ceil(serialized.length / CHARS_PER_TOKEN_ESTIMATE);\n}\n\nexport function withModelPerfProfile(streamFn: StreamFn, recorder: ModelPerfProfileStreamRecorder): StreamFn {\n\treturn async (model, context, streamOptions) => {\n\t\tconst nowMs = recorder.nowMs ?? Date.now;\n\t\tconst nowIso = recorder.nowIso ?? (() => new Date().toISOString());\n\t\tconst requestStartedAtMs = nowMs();\n\t\tlet responseHeadersAtMs: number | undefined;\n\t\tlet firstTokenAtMs: number | undefined;\n\t\tconst originalOnResponse = streamOptions?.onResponse;\n\t\tconst outer = createAssistantMessageEventStream();\n\t\tlet latest = emptyAssistantMessage(model);\n\t\tlet inner: Awaited<ReturnType<StreamFn>>;\n\t\ttry {\n\t\t\tinner = await streamFn(model, context, {\n\t\t\t\t...streamOptions,\n\t\t\t\tonResponse: async (response: ProviderResponse, responseModel: Model<Api>) => {\n\t\t\t\t\tresponseHeadersAtMs ??= nowMs();\n\t\t\t\t\tawait originalOnResponse?.(response, responseModel);\n\t\t\t\t},\n\t\t\t});\n\t\t} catch (error) {\n\t\t\touter.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true, error));\n\t\t\treturn outer;\n\t\t}\n\n\t\tvoid (async () => {\n\t\t\tlet terminalPushed = false;\n\t\t\ttry {\n\t\t\t\tfor await (const event of inner) {\n\t\t\t\t\tlatest = assistantMessageFromEvent(event);\n\t\t\t\t\tif (firstTokenAtMs === undefined && isFirstTokenEvent(event)) {\n\t\t\t\t\t\tfirstTokenAtMs = nowMs();\n\t\t\t\t\t}\n\t\t\t\t\tif (event.type === \"done\") {\n\t\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\t\trecordSuccessfulStreamSample({\n\t\t\t\t\t\t\tcompletionTokens: event.message.usage.output,\n\t\t\t\t\t\t\tdoneAtMs: nowMs(),\n\t\t\t\t\t\t\tfirstTokenAtMs,\n\t\t\t\t\t\t\tmodel,\n\t\t\t\t\t\t\tnowIso,\n\t\t\t\t\t\t\tpromptTokens:\n\t\t\t\t\t\t\t\tevent.message.usage.input + event.message.usage.cacheRead + event.message.usage.cacheWrite,\n\t\t\t\t\t\t\trecorder,\n\t\t\t\t\t\t\trequestStartedAtMs,\n\t\t\t\t\t\t\tresponseHeadersAtMs,\n\t\t\t\t\t\t});\n\t\t\t\t\t} else if (event.type === \"error\") {\n\t\t\t\t\t\tterminalPushed = true;\n\t\t\t\t\t}\n\t\t\t\t\touter.push(event);\n\t\t\t\t\tif (terminalPushed) return;\n\t\t\t\t}\n\t\t\t\touter.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true));\n\t\t\t} catch (error) {\n\t\t\t\touter.push(perfStreamFailure(latest, streamOptions?.signal?.aborted === true, error));\n\t\t\t}\n\t\t})();\n\n\t\treturn outer;\n\t};\n}\n\nfunction assistantMessageFromEvent(event: AssistantMessageEvent): AssistantMessage {\n\tif (event.type === \"done\") return event.message;\n\tif (event.type === \"error\") return event.error;\n\treturn event.partial;\n}\n\nfunction emptyAssistantMessage(model: Model<Api>): AssistantMessage {\n\treturn {\n\t\trole: \"assistant\",\n\t\tcontent: [],\n\t\tapi: model.api,\n\t\tprovider: model.provider,\n\t\tmodel: model.id,\n\t\tusage: {\n\t\t\tinput: 0,\n\t\t\toutput: 0,\n\t\t\tcacheRead: 0,\n\t\t\tcacheWrite: 0,\n\t\t\ttotalTokens: 0,\n\t\t\tcost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },\n\t\t},\n\t\tstopReason: \"stop\",\n\t\ttimestamp: Date.now(),\n\t};\n}\n\nfunction perfStreamFailure(latest: AssistantMessage, aborted: boolean, error?: unknown): AssistantMessageEvent {\n\tconst stopReason = aborted ? \"aborted\" : \"error\";\n\tconst detail = error instanceof Error ? `: ${error.message}` : error === undefined ? \"\" : `: ${String(error)}`;\n\treturn {\n\t\ttype: \"error\",\n\t\treason: stopReason,\n\t\terror: {\n\t\t\t...latest,\n\t\t\tstopReason,\n\t\t\terrorMessage: aborted\n\t\t\t\t? `stream aborted before terminal event${detail}`\n\t\t\t\t: `stream ended without terminal event${detail}`,\n\t\t},\n\t};\n}\n\nfunction recordSuccessfulStreamSample(input: {\n\tmodel: Model<Api>;\n\trecorder: ModelPerfProfileStreamRecorder;\n\trequestStartedAtMs: number;\n\tresponseHeadersAtMs: number | undefined;\n\tfirstTokenAtMs: number | undefined;\n\tdoneAtMs: number;\n\tpromptTokens: number;\n\tcompletionTokens: number;\n\tnowIso: () => string;\n}): void {\n\tif (input.firstTokenAtMs === undefined) return;\n\tconst modelKey = input.recorder.modelKey(input.model);\n\tif (!modelKey) return;\n\tconst requestToFirstTokenMs = input.firstTokenAtMs - input.requestStartedAtMs;\n\tconst sample: ModelPerfSample = {\n\t\tpromptTokens: input.promptTokens,\n\t\tcompletionTokens: input.completionTokens,\n\t\trequestToFirstTokenMs,\n\t\tfirstTokenToDoneMs: input.doneAtMs - input.firstTokenAtMs,\n\t\tat: input.nowIso(),\n\t};\n\tif (input.responseHeadersAtMs !== undefined) {\n\t\tconst headersToFirstTokenMs = input.firstTokenAtMs - input.responseHeadersAtMs;\n\t\tconst headersWereEffectivelyDeferred =\n\t\t\trequestToFirstTokenMs >= DEFERRED_HEADERS_MIN_REQUEST_MS &&\n\t\t\theadersToFirstTokenMs <= DEFERRED_HEADERS_MAX_GAP_MS;\n\t\tif (!headersWereEffectivelyDeferred) sample.headersToFirstTokenMs = headersToFirstTokenMs;\n\t}\n\tif (!hasUsableModelPerfSample(sample)) return;\n\ttry {\n\t\tinput.recorder.recordSample(modelKey, sample);\n\t} catch {\n\t\t// Perf profiling must never fail the user turn.\n\t}\n}\n\nfunction isFirstTokenEvent(event: AssistantMessageEvent): boolean {\n\treturn (\n\t\t(event.type === \"text_delta\" || event.type === \"thinking_delta\" || event.type === \"toolcall_delta\") &&\n\t\tevent.delta.length > 0\n\t);\n}\n\nfunction expectedPrefillFromProfile(profile: ModelPerfProfile | undefined, promptTokens: number): number | undefined {\n\tconst prefillRate = profile?.prefillTokensPerSecond;\n\tif (!profile || profile.samples < 1 || !prefillRate || promptTokens <= 0) return undefined;\n\tconst expectedPrefillMs = (promptTokens / prefillRate) * 1000;\n\treturn Number.isFinite(expectedPrefillMs) && expectedPrefillMs > 0 ? expectedPrefillMs : undefined;\n}\n\nfunction adaptiveBound(defaultMs: number, ceilingMs: number, expectedMs: number): number {\n\treturn Math.min(Math.ceil(Math.max(defaultMs, expectedMs * 3)), ceilingMs);\n}\n\nfunction prefillRateFromSample(sample: ModelPerfSample): number | undefined {\n\tconst durationMs = isPositiveFiniteNumber(sample.headersToFirstTokenMs)\n\t\t? sample.headersToFirstTokenMs\n\t\t: sample.requestToFirstTokenMs;\n\treturn tokensPerSecond(sample.promptTokens, durationMs);\n}\n\nfunction decodeRateFromSample(sample: ModelPerfSample): number | undefined {\n\treturn tokensPerSecond(sample.completionTokens, sample.firstTokenToDoneMs);\n}\n\nfunction tokensPerSecond(tokens: number | undefined, durationMs: number | undefined): number | undefined {\n\tif (!isPositiveFiniteNumber(tokens) || !isPositiveFiniteNumber(durationMs)) return undefined;\n\treturn tokens / (durationMs / 1000);\n}\n\nfunction updateEwma(previous: number | undefined, next: number, previousSamples: number): number {\n\tif (previous === undefined || previousSamples === 0) return next;\n\treturn previous * (1 - PERF_EWMA_ALPHA) + next * PERF_EWMA_ALPHA;\n}\n\nfunction isPositiveFiniteNumber(value: unknown): value is number {\n\treturn typeof value === \"number\" && Number.isFinite(value) && value > 0;\n}\n"]}
|
|
@@ -33,15 +33,26 @@ export interface ProfileFilterControllerDeps {
|
|
|
33
33
|
/** Apply a thinking level (delegates to model-selection). */
|
|
34
34
|
setThinkingLevel(level: ThinkingLevel): void;
|
|
35
35
|
}
|
|
36
|
+
export interface ProfileFilterReloadSnapshot {
|
|
37
|
+
inertExtensionWarnings: string[];
|
|
38
|
+
profileDeniedExtensionCount: number;
|
|
39
|
+
profileControlledModel: boolean;
|
|
40
|
+
profileControlledThinking: boolean;
|
|
41
|
+
}
|
|
36
42
|
export declare class ProfileFilterController {
|
|
37
43
|
/** G12: extensions loaded but rendered fully inert by the profile's tools filter. */
|
|
38
44
|
private _inertExtensionWarnings;
|
|
39
45
|
/** Count of extensions withheld by the active resource profile (for the /context observation). */
|
|
40
46
|
private _profileDeniedExtensionCount;
|
|
47
|
+
/** Whether the last applied profile generation, rather than the base session selection, owns the field. */
|
|
48
|
+
private _profileControlledModel;
|
|
49
|
+
private _profileControlledThinking;
|
|
41
50
|
private readonly deps;
|
|
42
51
|
constructor(deps: ProfileFilterControllerDeps);
|
|
43
52
|
/** Inert-extension warnings tracked by the last {@link filterExtensionsForRuntime} pass. */
|
|
44
53
|
getInertExtensionWarnings(): string[];
|
|
54
|
+
createReloadSnapshot(): ProfileFilterReloadSnapshot;
|
|
55
|
+
restoreReloadSnapshot(snapshot: ProfileFilterReloadSnapshot): void;
|
|
45
56
|
/**
|
|
46
57
|
* Resolve the active resource-profile tool allow/block filter from current settings.
|
|
47
58
|
* Mirrors the construction-time derivation (settingsManager.getResourceProfileFilter("tools"))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-filter-controller.d.ts","sourceRoot":"","sources":["../../src/core/profile-filter-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAEN,KAAK,6BAA6B,EAClC,KAAK,eAAe,EACpB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,2BAA2B;IAC3C,kBAAkB,IAAI,eAAe,CAAC;IACtC,iBAAiB,IAAI,cAAc,CAAC;IACpC,gBAAgB,IAAI,aAAa,CAAC;IAClC,MAAM,IAAI,MAAM,CAAC;IACjB,QAAQ,IAAI,KAAK,CAAC;IAClB,iBAAiB,IAAI,cAAc,CAAC;IACpC,yEAAyE;IACzE,mBAAmB,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC/C,0CAA0C;IAC1C,oBAAoB,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAChD,kFAAkF;IAClF,oBAAoB,IAAI,QAAQ,CAAC,6BAA6B,CAAC,GAAG,SAAS,CAAC;IAC5E,0FAA0F;IAC1F,eAAe,IAAI,OAAO,CAAC;IAC3B,qEAAqE;IACrE,kBAAkB,IAAI,OAAO,CAAC;IAC9B,6DAA6D;IAC7D,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;CAC7C;AAED,qBAAa,uBAAuB;IACnC,qFAAqF;IACrF,OAAO,CAAC,uBAAuB,CAAgB;IAC/C,kGAAkG;IAClG,OAAO,CAAC,4BAA4B,CAAK;IAEzC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IAEnD,YAAY,IAAI,EAAE,2BAA2B,EAE5C;IAED,4FAA4F;IAC5F,yBAAyB,IAAI,MAAM,EAAE,CAEpC;IAED;;;;OAIG;IACH,uBAAuB,IAAI,QAAQ,CAAC,6BAA6B,CAAC,CAGjE;IAED,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUrD;IAED,OAAO,CAAC,4BAA4B;IASpC,0BAA0B,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAuC/D;IAED;;;;;;;;;;;;;OAaG;IACH,iCAAiC,IAAI,MAAM,EAAE,CA0B5C;IAED;;;;;OAKG;IACG,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC,CA2BvD;CACD","sourcesContent":["/**\n * Resource-profile tool/extension gating + reload-time profile model re-application.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Resolves the active resource\n * profile's tool allow/block filter, filters the runtime extension set (tracking the withheld/inert\n * tallies it owns), reports the /context \"withheld by profile\" observations, and — on reload —\n * re-applies a profile-bound model/thinking level unless an explicit launch flag set them. Owns the\n * inert-extension warnings and profile-denied-extension count; everything else is read through deps.\n */\n\nimport type { Agent, ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport type { SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport type { Extension } from \"./extensions/index.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { resolveProfileModelSettings } from \"./model-resolver.ts\";\nimport type { ResourceLoader } from \"./resource-loader.ts\";\nimport {\n\tmatchesResourceProfilePattern,\n\ttype ResourceProfileFilterSettings,\n\ttype SettingsManager,\n} from \"./settings-manager.ts\";\n\nexport interface ProfileFilterControllerDeps {\n\tgetSettingsManager(): SettingsManager;\n\tgetResourceLoader(): ResourceLoader;\n\tgetModelRegistry(): ModelRegistry;\n\tgetCwd(): string;\n\tgetAgent(): Agent;\n\tgetSessionManager(): SessionManager;\n\t/** Construction-time tool allow-set (undefined = no allow-list gate). */\n\tgetAllowedToolNames(): Set<string> | undefined;\n\t/** Construction-time tool exclude-set. */\n\tgetExcludedToolNames(): Set<string> | undefined;\n\t/** Live resource-profile tool filter (re-derived on reload; may be undefined). */\n\tgetToolProfileFilter(): Required<ResourceProfileFilterSettings> | undefined;\n\t/** Whether the model was set by an explicit launch flag (profile must not clobber it). */\n\tisExplicitModel(): boolean;\n\t/** Whether the thinking level was set by an explicit launch flag. */\n\tisExplicitThinking(): boolean;\n\t/** Apply a thinking level (delegates to model-selection). */\n\tsetThinkingLevel(level: ThinkingLevel): void;\n}\n\nexport class ProfileFilterController {\n\t/** G12: extensions loaded but rendered fully inert by the profile's tools filter. */\n\tprivate _inertExtensionWarnings: string[] = [];\n\t/** Count of extensions withheld by the active resource profile (for the /context observation). */\n\tprivate _profileDeniedExtensionCount = 0;\n\n\tprivate readonly deps: ProfileFilterControllerDeps;\n\n\tconstructor(deps: ProfileFilterControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Inert-extension warnings tracked by the last {@link filterExtensionsForRuntime} pass. */\n\tgetInertExtensionWarnings(): string[] {\n\t\treturn this._inertExtensionWarnings;\n\t}\n\n\t/**\n\t * Resolve the active resource-profile tool allow/block filter from current settings.\n\t * Mirrors the construction-time derivation (settingsManager.getResourceProfileFilter(\"tools\"))\n\t * so reload() can re-apply it after a live settings/profile edit.\n\t */\n\tderiveToolProfileFilter(): Required<ResourceProfileFilterSettings> {\n\t\tconst filter = this.deps.getSettingsManager().getResourceProfileFilter(\"tools\");\n\t\treturn { allow: filter.allow ?? [], block: filter.block ?? [] };\n\t}\n\n\tisToolOrCommandAllowedByProfile(name: string): boolean {\n\t\tconst allowedToolNames = this.deps.getAllowedToolNames();\n\t\tconst excludedToolNames = this.deps.getExcludedToolNames();\n\t\tif (allowedToolNames && !allowedToolNames.has(name)) return false;\n\t\tif (excludedToolNames?.has(name)) return false;\n\t\tconst filter = this.deps.getToolProfileFilter();\n\t\tif (!filter) return true;\n\t\tif (filter.allow.length > 0 && !matchesResourceProfilePattern(name, filter.allow)) return false;\n\t\tif (matchesResourceProfilePattern(name, filter.block)) return false;\n\t\treturn true;\n\t}\n\n\tprivate _hasToolOrCommandProfileGate(): boolean {\n\t\tconst toolProfileFilter = this.deps.getToolProfileFilter();\n\t\treturn Boolean(\n\t\t\tthis.deps.getAllowedToolNames() ||\n\t\t\t\tthis.deps.getExcludedToolNames() ||\n\t\t\t\t(toolProfileFilter && (toolProfileFilter.allow.length > 0 || toolProfileFilter.block.length > 0)),\n\t\t);\n\t}\n\n\tfilterExtensionsForRuntime(extensions: Extension[]): Extension[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tthis._inertExtensionWarnings = [];\n\t\tthis._profileDeniedExtensionCount = 0;\n\t\tif (settingsManager.getActiveResourceProfileNames().length === 0) {\n\t\t\tif (settingsManager.hasExplicitActiveResourceProfileSelection()) {\n\t\t\t\t// An explicit profile selection that resolves to no active profile is a deliberate\n\t\t\t\t// deny-all — every extension is withheld by that choice.\n\t\t\t\tthis._profileDeniedExtensionCount = extensions.length;\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\t// No profile in play: only inline/SDK extensions load by default. That is the baseline, not\n\t\t\t// a profile denial, so it is not counted as withheld.\n\t\t\treturn extensions.filter((extension) => extension.sourceInfo.source === \"inline\");\n\t\t}\n\t\tconst hasToolOrCommandGate = this._hasToolOrCommandProfileGate();\n\t\tconst allowedExtensions = extensions.filter((extension) =>\n\t\t\tsettingsManager.isResourceAllowedByProfile(\"extensions\", extension.path, extension.sourceInfo.baseDir),\n\t\t);\n\t\tthis._profileDeniedExtensionCount = extensions.length - allowedExtensions.length;\n\t\treturn allowedExtensions.map((extension) => {\n\t\t\tif (!hasToolOrCommandGate) return extension;\n\t\t\tconst tools = new Map(\n\t\t\t\tArray.from(extension.tools.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\tconst commands = new Map(\n\t\t\t\tArray.from(extension.commands.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\t// G12: an extension the profile ALLOWS whose every tool and command the tools filter\n\t\t\t// then denies loads and runs lifecycle hooks but is completely uninvocable — surface\n\t\t\t// it instead of presenting a \"loaded\" extension that silently does nothing.\n\t\t\tif (extension.tools.size + extension.commands.size > 0 && tools.size + commands.size === 0) {\n\t\t\t\tconst name = extension.path.split(/[\\\\/]/).pop() ?? extension.path;\n\t\t\t\tthis._inertExtensionWarnings.push(\n\t\t\t\t\t`extension \"${name}\" is loaded but fully inert: the active profile's tools filter denies all ${extension.tools.size} tool(s) and ${extension.commands.size} command(s) it contributes`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { ...extension, tools, commands };\n\t\t});\n\t}\n\n\t/**\n\t * /context observations for skills/prompts/extensions the active resource profile removed from\n\t * listings — the analog of the withheld-AGENTS.md warning. Strict UAC makes these silently absent,\n\t * so a lean profile's effect on the resource surface stays visible. Counts are profile-scoped\n\t * (skills/prompts via the profile-independent discovery universe filtered by the live profile\n\t * filter; extensions via the runtime filter's denied tally). Empty when nothing is withheld.\n\t *\n\t * Uses `isResourceDeniedByActiveProfile` (profile-only), not `isResourceAllowedByProfile` (which\n\t * also folds in the user's own legacy `disabledResources` list): a plain user-disabled resource\n\t * must never be misattributed to \"the active resource profile\" — that case is already surfaced by\n\t * the G14 disable-wins warning. With no active profile at all, the helper always reports nothing\n\t * denied, so this naturally stays silent (extensions keep their own runtime-filter-derived count,\n\t * which is already correctly zero absent a profile).\n\t */\n\tprofileDeniedResourceObservations(): string[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst resourceLoader = this.deps.getResourceLoader();\n\t\tconst cwd = this.deps.getCwd();\n\t\tconst observations: string[] = [];\n\t\tconst withheld = (kind: \"skills\" | \"prompts\", paths: string[]): number =>\n\t\t\tpaths.filter((path) => settingsManager.isResourceDeniedByActiveProfile(kind, path, cwd)).length;\n\n\t\tconst skillsWithheld = withheld(\"skills\", resourceLoader.getDiscoverableSkillPaths());\n\t\tif (skillsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${skillsWithheld} skill(s) withheld by the active resource profile — grant the \"skills\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tconst promptsWithheld = withheld(\"prompts\", resourceLoader.getDiscoverablePromptPaths());\n\t\tif (promptsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${promptsWithheld} prompt(s) withheld by the active resource profile — grant the \"prompts\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tif (this._profileDeniedExtensionCount > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${this._profileDeniedExtensionCount} extension(s) withheld by the active resource profile — grant the \"extensions\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\treturn observations;\n\t}\n\n\t/**\n\t * Re-resolve the active resource profile's model/thinking from current settings and apply it.\n\t * Only acts when the profile actually binds model/thinking AND that field was not set by an\n\t * explicit launch flag — so live profile edits apply on reload without clobbering an explicit\n\t * --model/--thinking. A no-op for profiles that don't bind a model.\n\t */\n\tasync reapplyActiveProfileModelSettings(): Promise<void> {\n\t\tif (this.deps.isExplicitModel() && this.deps.isExplicitThinking()) return;\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst activeProfileNames = settingsManager.getActiveResourceProfileNames();\n\t\tif (activeProfileNames.length === 0) return;\n\t\tconst profileSettings = resolveProfileModelSettings({\n\t\t\tactiveProfileNames,\n\t\t\tregistry: settingsManager.getProfileRegistry(),\n\t\t\tmodelRegistry: this.deps.getModelRegistry(),\n\t\t\tcwd: this.deps.getCwd(),\n\t\t});\n\t\tif (!this.deps.isExplicitModel() && profileSettings.model) {\n\t\t\tconst current = this.deps.getAgent().state.model;\n\t\t\tconst next = profileSettings.model;\n\t\t\tif (!current || current.provider !== next.provider || current.id !== next.id) {\n\t\t\t\t// Mirror the startup/cycle path: set the model directly (no auth gate, no settings\n\t\t\t\t// persist) so re-applying the profile model behaves like initial resolution rather\n\t\t\t\t// than a runtime model switch. No model_select emit here — reload rebuilds the\n\t\t\t\t// extension runtime and emits session_start(\"reload\") right after, and the UI\n\t\t\t\t// re-renders from session.model.\n\t\t\t\tthis.deps.getAgent().state.model = next;\n\t\t\t\tthis.deps.getSessionManager().appendModelChange(next.provider, next.id);\n\t\t\t}\n\t\t}\n\t\tif (!this.deps.isExplicitThinking() && profileSettings.thinkingLevel) {\n\t\t\tthis.deps.setThinkingLevel(profileSettings.thinkingLevel);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"profile-filter-controller.d.ts","sourceRoot":"","sources":["../../src/core/profile-filter-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAEN,KAAK,6BAA6B,EAClC,KAAK,eAAe,EACpB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,WAAW,2BAA2B;IAC3C,kBAAkB,IAAI,eAAe,CAAC;IACtC,iBAAiB,IAAI,cAAc,CAAC;IACpC,gBAAgB,IAAI,aAAa,CAAC;IAClC,MAAM,IAAI,MAAM,CAAC;IACjB,QAAQ,IAAI,KAAK,CAAC;IAClB,iBAAiB,IAAI,cAAc,CAAC;IACpC,yEAAyE;IACzE,mBAAmB,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAC/C,0CAA0C;IAC1C,oBAAoB,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;IAChD,kFAAkF;IAClF,oBAAoB,IAAI,QAAQ,CAAC,6BAA6B,CAAC,GAAG,SAAS,CAAC;IAC5E,0FAA0F;IAC1F,eAAe,IAAI,OAAO,CAAC;IAC3B,qEAAqE;IACrE,kBAAkB,IAAI,OAAO,CAAC;IAC9B,6DAA6D;IAC7D,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI,CAAC;CAC7C;AAED,MAAM,WAAW,2BAA2B;IAC3C,sBAAsB,EAAE,MAAM,EAAE,CAAC;IACjC,2BAA2B,EAAE,MAAM,CAAC;IACpC,sBAAsB,EAAE,OAAO,CAAC;IAChC,yBAAyB,EAAE,OAAO,CAAC;CACnC;AAED,qBAAa,uBAAuB;IACnC,qFAAqF;IACrF,OAAO,CAAC,uBAAuB,CAAgB;IAC/C,kGAAkG;IAClG,OAAO,CAAC,4BAA4B,CAAK;IACzC,2GAA2G;IAC3G,OAAO,CAAC,uBAAuB,CAAS;IACxC,OAAO,CAAC,0BAA0B,CAAS;IAE3C,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA8B;IAEnD,YAAY,IAAI,EAAE,2BAA2B,EAE5C;IAED,4FAA4F;IAC5F,yBAAyB,IAAI,MAAM,EAAE,CAEpC;IAED,oBAAoB,IAAI,2BAA2B,CAOlD;IAED,qBAAqB,CAAC,QAAQ,EAAE,2BAA2B,GAAG,IAAI,CAKjE;IAED;;;;OAIG;IACH,uBAAuB,IAAI,QAAQ,CAAC,6BAA6B,CAAC,CAGjE;IAED,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUrD;IAED,OAAO,CAAC,4BAA4B;IASpC,0BAA0B,CAAC,UAAU,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,CAuC/D;IAED;;;;;;;;;;;;;OAaG;IACH,iCAAiC,IAAI,MAAM,EAAE,CA0B5C;IAED;;;;;OAKG;IACG,iCAAiC,IAAI,OAAO,CAAC,IAAI,CAAC,CA0DvD;CACD","sourcesContent":["/**\n * Resource-profile tool/extension gating + reload-time profile model re-application.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Resolves the active resource\n * profile's tool allow/block filter, filters the runtime extension set (tracking the withheld/inert\n * tallies it owns), reports the /context \"withheld by profile\" observations, and — on reload —\n * re-applies a profile-bound model/thinking level unless an explicit launch flag set them. Owns the\n * inert-extension warnings and profile-denied-extension count; everything else is read through deps.\n */\n\nimport type { Agent, ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport type { SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport type { Extension } from \"./extensions/index.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { findInitialModel, resolveProfileModelSettings } from \"./model-resolver.ts\";\nimport type { ResourceLoader } from \"./resource-loader.ts\";\nimport {\n\tmatchesResourceProfilePattern,\n\ttype ResourceProfileFilterSettings,\n\ttype SettingsManager,\n} from \"./settings-manager.ts\";\n\nexport interface ProfileFilterControllerDeps {\n\tgetSettingsManager(): SettingsManager;\n\tgetResourceLoader(): ResourceLoader;\n\tgetModelRegistry(): ModelRegistry;\n\tgetCwd(): string;\n\tgetAgent(): Agent;\n\tgetSessionManager(): SessionManager;\n\t/** Construction-time tool allow-set (undefined = no allow-list gate). */\n\tgetAllowedToolNames(): Set<string> | undefined;\n\t/** Construction-time tool exclude-set. */\n\tgetExcludedToolNames(): Set<string> | undefined;\n\t/** Live resource-profile tool filter (re-derived on reload; may be undefined). */\n\tgetToolProfileFilter(): Required<ResourceProfileFilterSettings> | undefined;\n\t/** Whether the model was set by an explicit launch flag (profile must not clobber it). */\n\tisExplicitModel(): boolean;\n\t/** Whether the thinking level was set by an explicit launch flag. */\n\tisExplicitThinking(): boolean;\n\t/** Apply a thinking level (delegates to model-selection). */\n\tsetThinkingLevel(level: ThinkingLevel): void;\n}\n\nexport interface ProfileFilterReloadSnapshot {\n\tinertExtensionWarnings: string[];\n\tprofileDeniedExtensionCount: number;\n\tprofileControlledModel: boolean;\n\tprofileControlledThinking: boolean;\n}\n\nexport class ProfileFilterController {\n\t/** G12: extensions loaded but rendered fully inert by the profile's tools filter. */\n\tprivate _inertExtensionWarnings: string[] = [];\n\t/** Count of extensions withheld by the active resource profile (for the /context observation). */\n\tprivate _profileDeniedExtensionCount = 0;\n\t/** Whether the last applied profile generation, rather than the base session selection, owns the field. */\n\tprivate _profileControlledModel = false;\n\tprivate _profileControlledThinking = false;\n\n\tprivate readonly deps: ProfileFilterControllerDeps;\n\n\tconstructor(deps: ProfileFilterControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Inert-extension warnings tracked by the last {@link filterExtensionsForRuntime} pass. */\n\tgetInertExtensionWarnings(): string[] {\n\t\treturn this._inertExtensionWarnings;\n\t}\n\n\tcreateReloadSnapshot(): ProfileFilterReloadSnapshot {\n\t\treturn {\n\t\t\tinertExtensionWarnings: [...this._inertExtensionWarnings],\n\t\t\tprofileDeniedExtensionCount: this._profileDeniedExtensionCount,\n\t\t\tprofileControlledModel: this._profileControlledModel,\n\t\t\tprofileControlledThinking: this._profileControlledThinking,\n\t\t};\n\t}\n\n\trestoreReloadSnapshot(snapshot: ProfileFilterReloadSnapshot): void {\n\t\tthis._inertExtensionWarnings = [...snapshot.inertExtensionWarnings];\n\t\tthis._profileDeniedExtensionCount = snapshot.profileDeniedExtensionCount;\n\t\tthis._profileControlledModel = snapshot.profileControlledModel;\n\t\tthis._profileControlledThinking = snapshot.profileControlledThinking;\n\t}\n\n\t/**\n\t * Resolve the active resource-profile tool allow/block filter from current settings.\n\t * Mirrors the construction-time derivation (settingsManager.getResourceProfileFilter(\"tools\"))\n\t * so reload() can re-apply it after a live settings/profile edit.\n\t */\n\tderiveToolProfileFilter(): Required<ResourceProfileFilterSettings> {\n\t\tconst filter = this.deps.getSettingsManager().getResourceProfileFilter(\"tools\");\n\t\treturn { allow: filter.allow ?? [], block: filter.block ?? [] };\n\t}\n\n\tisToolOrCommandAllowedByProfile(name: string): boolean {\n\t\tconst allowedToolNames = this.deps.getAllowedToolNames();\n\t\tconst excludedToolNames = this.deps.getExcludedToolNames();\n\t\tif (allowedToolNames && !allowedToolNames.has(name)) return false;\n\t\tif (excludedToolNames?.has(name)) return false;\n\t\tconst filter = this.deps.getToolProfileFilter();\n\t\tif (!filter) return true;\n\t\tif (filter.allow.length > 0 && !matchesResourceProfilePattern(name, filter.allow)) return false;\n\t\tif (matchesResourceProfilePattern(name, filter.block)) return false;\n\t\treturn true;\n\t}\n\n\tprivate _hasToolOrCommandProfileGate(): boolean {\n\t\tconst toolProfileFilter = this.deps.getToolProfileFilter();\n\t\treturn Boolean(\n\t\t\tthis.deps.getAllowedToolNames() ||\n\t\t\t\tthis.deps.getExcludedToolNames() ||\n\t\t\t\t(toolProfileFilter && (toolProfileFilter.allow.length > 0 || toolProfileFilter.block.length > 0)),\n\t\t);\n\t}\n\n\tfilterExtensionsForRuntime(extensions: Extension[]): Extension[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tthis._inertExtensionWarnings = [];\n\t\tthis._profileDeniedExtensionCount = 0;\n\t\tif (settingsManager.getActiveResourceProfileNames().length === 0) {\n\t\t\tif (settingsManager.hasExplicitActiveResourceProfileSelection()) {\n\t\t\t\t// An explicit profile selection that resolves to no active profile is a deliberate\n\t\t\t\t// deny-all — every extension is withheld by that choice.\n\t\t\t\tthis._profileDeniedExtensionCount = extensions.length;\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\t// No profile in play: only inline/SDK extensions load by default. That is the baseline, not\n\t\t\t// a profile denial, so it is not counted as withheld.\n\t\t\treturn extensions.filter((extension) => extension.sourceInfo.source === \"inline\");\n\t\t}\n\t\tconst hasToolOrCommandGate = this._hasToolOrCommandProfileGate();\n\t\tconst allowedExtensions = extensions.filter((extension) =>\n\t\t\tsettingsManager.isResourceAllowedByProfile(\"extensions\", extension.path, extension.sourceInfo.baseDir),\n\t\t);\n\t\tthis._profileDeniedExtensionCount = extensions.length - allowedExtensions.length;\n\t\treturn allowedExtensions.map((extension) => {\n\t\t\tif (!hasToolOrCommandGate) return extension;\n\t\t\tconst tools = new Map(\n\t\t\t\tArray.from(extension.tools.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\tconst commands = new Map(\n\t\t\t\tArray.from(extension.commands.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\t// G12: an extension the profile ALLOWS whose every tool and command the tools filter\n\t\t\t// then denies loads and runs lifecycle hooks but is completely uninvocable — surface\n\t\t\t// it instead of presenting a \"loaded\" extension that silently does nothing.\n\t\t\tif (extension.tools.size + extension.commands.size > 0 && tools.size + commands.size === 0) {\n\t\t\t\tconst name = extension.path.split(/[\\\\/]/).pop() ?? extension.path;\n\t\t\t\tthis._inertExtensionWarnings.push(\n\t\t\t\t\t`extension \"${name}\" is loaded but fully inert: the active profile's tools filter denies all ${extension.tools.size} tool(s) and ${extension.commands.size} command(s) it contributes`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { ...extension, tools, commands };\n\t\t});\n\t}\n\n\t/**\n\t * /context observations for skills/prompts/extensions the active resource profile removed from\n\t * listings — the analog of the withheld-AGENTS.md warning. Strict UAC makes these silently absent,\n\t * so a lean profile's effect on the resource surface stays visible. Counts are profile-scoped\n\t * (skills/prompts via the profile-independent discovery universe filtered by the live profile\n\t * filter; extensions via the runtime filter's denied tally). Empty when nothing is withheld.\n\t *\n\t * Uses `isResourceDeniedByActiveProfile` (profile-only), not `isResourceAllowedByProfile` (which\n\t * also folds in the user's own legacy `disabledResources` list): a plain user-disabled resource\n\t * must never be misattributed to \"the active resource profile\" — that case is already surfaced by\n\t * the G14 disable-wins warning. With no active profile at all, the helper always reports nothing\n\t * denied, so this naturally stays silent (extensions keep their own runtime-filter-derived count,\n\t * which is already correctly zero absent a profile).\n\t */\n\tprofileDeniedResourceObservations(): string[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst resourceLoader = this.deps.getResourceLoader();\n\t\tconst cwd = this.deps.getCwd();\n\t\tconst observations: string[] = [];\n\t\tconst withheld = (kind: \"skills\" | \"prompts\", paths: string[]): number =>\n\t\t\tpaths.filter((path) => settingsManager.isResourceDeniedByActiveProfile(kind, path, cwd)).length;\n\n\t\tconst skillsWithheld = withheld(\"skills\", resourceLoader.getDiscoverableSkillPaths());\n\t\tif (skillsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${skillsWithheld} skill(s) withheld by the active resource profile — grant the \"skills\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tconst promptsWithheld = withheld(\"prompts\", resourceLoader.getDiscoverablePromptPaths());\n\t\tif (promptsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${promptsWithheld} prompt(s) withheld by the active resource profile — grant the \"prompts\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tif (this._profileDeniedExtensionCount > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${this._profileDeniedExtensionCount} extension(s) withheld by the active resource profile — grant the \"extensions\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\treturn observations;\n\t}\n\n\t/**\n\t * Re-resolve the active resource profile's model/thinking from current settings and apply it.\n\t * Only acts when the profile actually binds model/thinking AND that field was not set by an\n\t * explicit launch flag — so live profile edits apply on reload without clobbering an explicit\n\t * --model/--thinking. A no-op for profiles that don't bind a model.\n\t */\n\tasync reapplyActiveProfileModelSettings(): Promise<void> {\n\t\tif (this.deps.isExplicitModel() && this.deps.isExplicitThinking()) return;\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst activeProfileNames = settingsManager.getActiveResourceProfileNames();\n\t\tconst profileSettings =\n\t\t\tactiveProfileNames.length > 0\n\t\t\t\t? resolveProfileModelSettings({\n\t\t\t\t\t\tactiveProfileNames,\n\t\t\t\t\t\tregistry: settingsManager.getProfileRegistry(),\n\t\t\t\t\t\tmodelRegistry: this.deps.getModelRegistry(),\n\t\t\t\t\t\tcwd: this.deps.getCwd(),\n\t\t\t\t\t})\n\t\t\t\t: {};\n\t\tif (profileSettings.error) {\n\t\t\tthrow new Error(`Profile model resolution error: ${profileSettings.error}`);\n\t\t}\n\t\tconst mustRestoreBaseModel =\n\t\t\t!this.deps.isExplicitModel() && this._profileControlledModel && !profileSettings.model;\n\t\tconst mustRestoreBaseThinking =\n\t\t\t!this.deps.isExplicitThinking() && this._profileControlledThinking && !profileSettings.thinkingLevel;\n\t\tconst baseSelection =\n\t\t\tmustRestoreBaseModel || mustRestoreBaseThinking\n\t\t\t\t? await findInitialModel({\n\t\t\t\t\t\tscopedModels: [],\n\t\t\t\t\t\tisContinuing: true,\n\t\t\t\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\t\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\t\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\t\t\t\tmodelRegistry: this.deps.getModelRegistry(),\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\tlet modelChanged = false;\n\t\tconst nextModel = profileSettings.model ?? (mustRestoreBaseModel ? baseSelection?.model : undefined);\n\t\tif (!this.deps.isExplicitModel() && nextModel) {\n\t\t\tconst current = this.deps.getAgent().state.model;\n\t\t\tconst next = nextModel;\n\t\t\tif (!current || current.provider !== next.provider || current.id !== next.id) {\n\t\t\t\t// Mirror the startup/cycle path: set the model directly (no auth gate, no settings\n\t\t\t\t// persist) so re-applying the profile model behaves like initial resolution rather\n\t\t\t\t// than a runtime model switch. No model_select emit here — reload rebuilds the\n\t\t\t\t// extension runtime and emits session_start(\"reload\") right after, and the UI\n\t\t\t\t// re-renders from session.model.\n\t\t\t\tthis.deps.getAgent().state.model = next;\n\t\t\t\tthis.deps.getSessionManager().appendModelChange(next.provider, next.id);\n\t\t\t\tmodelChanged = true;\n\t\t\t}\n\t\t}\n\t\tthis._profileControlledModel = !this.deps.isExplicitModel() && Boolean(profileSettings.model);\n\t\tif (!this.deps.isExplicitThinking() && profileSettings.thinkingLevel) {\n\t\t\tthis.deps.setThinkingLevel(profileSettings.thinkingLevel);\n\t\t} else if (mustRestoreBaseThinking && baseSelection) {\n\t\t\tthis.deps.setThinkingLevel(baseSelection.thinkingLevel);\n\t\t} else if (modelChanged) {\n\t\t\t// A model-only situation switch still has to clamp the inherited/explicit level. This\n\t\t\t// prevents Sol/Terra Ultra from leaking into Luna or another model that stops at Max.\n\t\t\tthis.deps.setThinkingLevel(this.deps.getAgent().state.thinkingLevel);\n\t\t}\n\t\tthis._profileControlledThinking = !this.deps.isExplicitThinking() && Boolean(profileSettings.thinkingLevel);\n\t}\n}\n"]}
|
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
* re-applies a profile-bound model/thinking level unless an explicit launch flag set them. Owns the
|
|
8
8
|
* inert-extension warnings and profile-denied-extension count; everything else is read through deps.
|
|
9
9
|
*/
|
|
10
|
-
import { resolveProfileModelSettings } from "./model-resolver.js";
|
|
10
|
+
import { findInitialModel, resolveProfileModelSettings } from "./model-resolver.js";
|
|
11
11
|
import { matchesResourceProfilePattern, } from "./settings-manager.js";
|
|
12
12
|
export class ProfileFilterController {
|
|
13
13
|
/** G12: extensions loaded but rendered fully inert by the profile's tools filter. */
|
|
14
14
|
_inertExtensionWarnings = [];
|
|
15
15
|
/** Count of extensions withheld by the active resource profile (for the /context observation). */
|
|
16
16
|
_profileDeniedExtensionCount = 0;
|
|
17
|
+
/** Whether the last applied profile generation, rather than the base session selection, owns the field. */
|
|
18
|
+
_profileControlledModel = false;
|
|
19
|
+
_profileControlledThinking = false;
|
|
17
20
|
deps;
|
|
18
21
|
constructor(deps) {
|
|
19
22
|
this.deps = deps;
|
|
@@ -22,6 +25,20 @@ export class ProfileFilterController {
|
|
|
22
25
|
getInertExtensionWarnings() {
|
|
23
26
|
return this._inertExtensionWarnings;
|
|
24
27
|
}
|
|
28
|
+
createReloadSnapshot() {
|
|
29
|
+
return {
|
|
30
|
+
inertExtensionWarnings: [...this._inertExtensionWarnings],
|
|
31
|
+
profileDeniedExtensionCount: this._profileDeniedExtensionCount,
|
|
32
|
+
profileControlledModel: this._profileControlledModel,
|
|
33
|
+
profileControlledThinking: this._profileControlledThinking,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
restoreReloadSnapshot(snapshot) {
|
|
37
|
+
this._inertExtensionWarnings = [...snapshot.inertExtensionWarnings];
|
|
38
|
+
this._profileDeniedExtensionCount = snapshot.profileDeniedExtensionCount;
|
|
39
|
+
this._profileControlledModel = snapshot.profileControlledModel;
|
|
40
|
+
this._profileControlledThinking = snapshot.profileControlledThinking;
|
|
41
|
+
}
|
|
25
42
|
/**
|
|
26
43
|
* Resolve the active resource-profile tool allow/block filter from current settings.
|
|
27
44
|
* Mirrors the construction-time derivation (settingsManager.getResourceProfileFilter("tools"))
|
|
@@ -130,17 +147,34 @@ export class ProfileFilterController {
|
|
|
130
147
|
return;
|
|
131
148
|
const settingsManager = this.deps.getSettingsManager();
|
|
132
149
|
const activeProfileNames = settingsManager.getActiveResourceProfileNames();
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (
|
|
150
|
+
const profileSettings = activeProfileNames.length > 0
|
|
151
|
+
? resolveProfileModelSettings({
|
|
152
|
+
activeProfileNames,
|
|
153
|
+
registry: settingsManager.getProfileRegistry(),
|
|
154
|
+
modelRegistry: this.deps.getModelRegistry(),
|
|
155
|
+
cwd: this.deps.getCwd(),
|
|
156
|
+
})
|
|
157
|
+
: {};
|
|
158
|
+
if (profileSettings.error) {
|
|
159
|
+
throw new Error(`Profile model resolution error: ${profileSettings.error}`);
|
|
160
|
+
}
|
|
161
|
+
const mustRestoreBaseModel = !this.deps.isExplicitModel() && this._profileControlledModel && !profileSettings.model;
|
|
162
|
+
const mustRestoreBaseThinking = !this.deps.isExplicitThinking() && this._profileControlledThinking && !profileSettings.thinkingLevel;
|
|
163
|
+
const baseSelection = mustRestoreBaseModel || mustRestoreBaseThinking
|
|
164
|
+
? await findInitialModel({
|
|
165
|
+
scopedModels: [],
|
|
166
|
+
isContinuing: true,
|
|
167
|
+
defaultProvider: settingsManager.getDefaultProvider(),
|
|
168
|
+
defaultModelId: settingsManager.getDefaultModel(),
|
|
169
|
+
defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),
|
|
170
|
+
modelRegistry: this.deps.getModelRegistry(),
|
|
171
|
+
})
|
|
172
|
+
: undefined;
|
|
173
|
+
let modelChanged = false;
|
|
174
|
+
const nextModel = profileSettings.model ?? (mustRestoreBaseModel ? baseSelection?.model : undefined);
|
|
175
|
+
if (!this.deps.isExplicitModel() && nextModel) {
|
|
142
176
|
const current = this.deps.getAgent().state.model;
|
|
143
|
-
const next =
|
|
177
|
+
const next = nextModel;
|
|
144
178
|
if (!current || current.provider !== next.provider || current.id !== next.id) {
|
|
145
179
|
// Mirror the startup/cycle path: set the model directly (no auth gate, no settings
|
|
146
180
|
// persist) so re-applying the profile model behaves like initial resolution rather
|
|
@@ -149,11 +183,22 @@ export class ProfileFilterController {
|
|
|
149
183
|
// re-renders from session.model.
|
|
150
184
|
this.deps.getAgent().state.model = next;
|
|
151
185
|
this.deps.getSessionManager().appendModelChange(next.provider, next.id);
|
|
186
|
+
modelChanged = true;
|
|
152
187
|
}
|
|
153
188
|
}
|
|
189
|
+
this._profileControlledModel = !this.deps.isExplicitModel() && Boolean(profileSettings.model);
|
|
154
190
|
if (!this.deps.isExplicitThinking() && profileSettings.thinkingLevel) {
|
|
155
191
|
this.deps.setThinkingLevel(profileSettings.thinkingLevel);
|
|
156
192
|
}
|
|
193
|
+
else if (mustRestoreBaseThinking && baseSelection) {
|
|
194
|
+
this.deps.setThinkingLevel(baseSelection.thinkingLevel);
|
|
195
|
+
}
|
|
196
|
+
else if (modelChanged) {
|
|
197
|
+
// A model-only situation switch still has to clamp the inherited/explicit level. This
|
|
198
|
+
// prevents Sol/Terra Ultra from leaking into Luna or another model that stops at Max.
|
|
199
|
+
this.deps.setThinkingLevel(this.deps.getAgent().state.thinkingLevel);
|
|
200
|
+
}
|
|
201
|
+
this._profileControlledThinking = !this.deps.isExplicitThinking() && Boolean(profileSettings.thinkingLevel);
|
|
157
202
|
}
|
|
158
203
|
}
|
|
159
204
|
//# sourceMappingURL=profile-filter-controller.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"profile-filter-controller.js","sourceRoot":"","sources":["../../src/core/profile-filter-controller.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Resource-profile tool/extension gating + reload-time profile model re-application.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Resolves the active resource\n * profile's tool allow/block filter, filters the runtime extension set (tracking the withheld/inert\n * tallies it owns), reports the /context \"withheld by profile\" observations, and — on reload —\n * re-applies a profile-bound model/thinking level unless an explicit launch flag set them. Owns the\n * inert-extension warnings and profile-denied-extension count; everything else is read through deps.\n */\n\nimport type { Agent, ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport type { SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport type { Extension } from \"./extensions/index.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { resolveProfileModelSettings } from \"./model-resolver.ts\";\nimport type { ResourceLoader } from \"./resource-loader.ts\";\nimport {\n\tmatchesResourceProfilePattern,\n\ttype ResourceProfileFilterSettings,\n\ttype SettingsManager,\n} from \"./settings-manager.ts\";\n\nexport interface ProfileFilterControllerDeps {\n\tgetSettingsManager(): SettingsManager;\n\tgetResourceLoader(): ResourceLoader;\n\tgetModelRegistry(): ModelRegistry;\n\tgetCwd(): string;\n\tgetAgent(): Agent;\n\tgetSessionManager(): SessionManager;\n\t/** Construction-time tool allow-set (undefined = no allow-list gate). */\n\tgetAllowedToolNames(): Set<string> | undefined;\n\t/** Construction-time tool exclude-set. */\n\tgetExcludedToolNames(): Set<string> | undefined;\n\t/** Live resource-profile tool filter (re-derived on reload; may be undefined). */\n\tgetToolProfileFilter(): Required<ResourceProfileFilterSettings> | undefined;\n\t/** Whether the model was set by an explicit launch flag (profile must not clobber it). */\n\tisExplicitModel(): boolean;\n\t/** Whether the thinking level was set by an explicit launch flag. */\n\tisExplicitThinking(): boolean;\n\t/** Apply a thinking level (delegates to model-selection). */\n\tsetThinkingLevel(level: ThinkingLevel): void;\n}\n\nexport class ProfileFilterController {\n\t/** G12: extensions loaded but rendered fully inert by the profile's tools filter. */\n\tprivate _inertExtensionWarnings: string[] = [];\n\t/** Count of extensions withheld by the active resource profile (for the /context observation). */\n\tprivate _profileDeniedExtensionCount = 0;\n\n\tprivate readonly deps: ProfileFilterControllerDeps;\n\n\tconstructor(deps: ProfileFilterControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Inert-extension warnings tracked by the last {@link filterExtensionsForRuntime} pass. */\n\tgetInertExtensionWarnings(): string[] {\n\t\treturn this._inertExtensionWarnings;\n\t}\n\n\t/**\n\t * Resolve the active resource-profile tool allow/block filter from current settings.\n\t * Mirrors the construction-time derivation (settingsManager.getResourceProfileFilter(\"tools\"))\n\t * so reload() can re-apply it after a live settings/profile edit.\n\t */\n\tderiveToolProfileFilter(): Required<ResourceProfileFilterSettings> {\n\t\tconst filter = this.deps.getSettingsManager().getResourceProfileFilter(\"tools\");\n\t\treturn { allow: filter.allow ?? [], block: filter.block ?? [] };\n\t}\n\n\tisToolOrCommandAllowedByProfile(name: string): boolean {\n\t\tconst allowedToolNames = this.deps.getAllowedToolNames();\n\t\tconst excludedToolNames = this.deps.getExcludedToolNames();\n\t\tif (allowedToolNames && !allowedToolNames.has(name)) return false;\n\t\tif (excludedToolNames?.has(name)) return false;\n\t\tconst filter = this.deps.getToolProfileFilter();\n\t\tif (!filter) return true;\n\t\tif (filter.allow.length > 0 && !matchesResourceProfilePattern(name, filter.allow)) return false;\n\t\tif (matchesResourceProfilePattern(name, filter.block)) return false;\n\t\treturn true;\n\t}\n\n\tprivate _hasToolOrCommandProfileGate(): boolean {\n\t\tconst toolProfileFilter = this.deps.getToolProfileFilter();\n\t\treturn Boolean(\n\t\t\tthis.deps.getAllowedToolNames() ||\n\t\t\t\tthis.deps.getExcludedToolNames() ||\n\t\t\t\t(toolProfileFilter && (toolProfileFilter.allow.length > 0 || toolProfileFilter.block.length > 0)),\n\t\t);\n\t}\n\n\tfilterExtensionsForRuntime(extensions: Extension[]): Extension[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tthis._inertExtensionWarnings = [];\n\t\tthis._profileDeniedExtensionCount = 0;\n\t\tif (settingsManager.getActiveResourceProfileNames().length === 0) {\n\t\t\tif (settingsManager.hasExplicitActiveResourceProfileSelection()) {\n\t\t\t\t// An explicit profile selection that resolves to no active profile is a deliberate\n\t\t\t\t// deny-all — every extension is withheld by that choice.\n\t\t\t\tthis._profileDeniedExtensionCount = extensions.length;\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\t// No profile in play: only inline/SDK extensions load by default. That is the baseline, not\n\t\t\t// a profile denial, so it is not counted as withheld.\n\t\t\treturn extensions.filter((extension) => extension.sourceInfo.source === \"inline\");\n\t\t}\n\t\tconst hasToolOrCommandGate = this._hasToolOrCommandProfileGate();\n\t\tconst allowedExtensions = extensions.filter((extension) =>\n\t\t\tsettingsManager.isResourceAllowedByProfile(\"extensions\", extension.path, extension.sourceInfo.baseDir),\n\t\t);\n\t\tthis._profileDeniedExtensionCount = extensions.length - allowedExtensions.length;\n\t\treturn allowedExtensions.map((extension) => {\n\t\t\tif (!hasToolOrCommandGate) return extension;\n\t\t\tconst tools = new Map(\n\t\t\t\tArray.from(extension.tools.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\tconst commands = new Map(\n\t\t\t\tArray.from(extension.commands.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\t// G12: an extension the profile ALLOWS whose every tool and command the tools filter\n\t\t\t// then denies loads and runs lifecycle hooks but is completely uninvocable — surface\n\t\t\t// it instead of presenting a \"loaded\" extension that silently does nothing.\n\t\t\tif (extension.tools.size + extension.commands.size > 0 && tools.size + commands.size === 0) {\n\t\t\t\tconst name = extension.path.split(/[\\\\/]/).pop() ?? extension.path;\n\t\t\t\tthis._inertExtensionWarnings.push(\n\t\t\t\t\t`extension \"${name}\" is loaded but fully inert: the active profile's tools filter denies all ${extension.tools.size} tool(s) and ${extension.commands.size} command(s) it contributes`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { ...extension, tools, commands };\n\t\t});\n\t}\n\n\t/**\n\t * /context observations for skills/prompts/extensions the active resource profile removed from\n\t * listings — the analog of the withheld-AGENTS.md warning. Strict UAC makes these silently absent,\n\t * so a lean profile's effect on the resource surface stays visible. Counts are profile-scoped\n\t * (skills/prompts via the profile-independent discovery universe filtered by the live profile\n\t * filter; extensions via the runtime filter's denied tally). Empty when nothing is withheld.\n\t *\n\t * Uses `isResourceDeniedByActiveProfile` (profile-only), not `isResourceAllowedByProfile` (which\n\t * also folds in the user's own legacy `disabledResources` list): a plain user-disabled resource\n\t * must never be misattributed to \"the active resource profile\" — that case is already surfaced by\n\t * the G14 disable-wins warning. With no active profile at all, the helper always reports nothing\n\t * denied, so this naturally stays silent (extensions keep their own runtime-filter-derived count,\n\t * which is already correctly zero absent a profile).\n\t */\n\tprofileDeniedResourceObservations(): string[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst resourceLoader = this.deps.getResourceLoader();\n\t\tconst cwd = this.deps.getCwd();\n\t\tconst observations: string[] = [];\n\t\tconst withheld = (kind: \"skills\" | \"prompts\", paths: string[]): number =>\n\t\t\tpaths.filter((path) => settingsManager.isResourceDeniedByActiveProfile(kind, path, cwd)).length;\n\n\t\tconst skillsWithheld = withheld(\"skills\", resourceLoader.getDiscoverableSkillPaths());\n\t\tif (skillsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${skillsWithheld} skill(s) withheld by the active resource profile — grant the \"skills\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tconst promptsWithheld = withheld(\"prompts\", resourceLoader.getDiscoverablePromptPaths());\n\t\tif (promptsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${promptsWithheld} prompt(s) withheld by the active resource profile — grant the \"prompts\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tif (this._profileDeniedExtensionCount > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${this._profileDeniedExtensionCount} extension(s) withheld by the active resource profile — grant the \"extensions\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\treturn observations;\n\t}\n\n\t/**\n\t * Re-resolve the active resource profile's model/thinking from current settings and apply it.\n\t * Only acts when the profile actually binds model/thinking AND that field was not set by an\n\t * explicit launch flag — so live profile edits apply on reload without clobbering an explicit\n\t * --model/--thinking. A no-op for profiles that don't bind a model.\n\t */\n\tasync reapplyActiveProfileModelSettings(): Promise<void> {\n\t\tif (this.deps.isExplicitModel() && this.deps.isExplicitThinking()) return;\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst activeProfileNames = settingsManager.getActiveResourceProfileNames();\n\t\tif (activeProfileNames.length === 0) return;\n\t\tconst profileSettings = resolveProfileModelSettings({\n\t\t\tactiveProfileNames,\n\t\t\tregistry: settingsManager.getProfileRegistry(),\n\t\t\tmodelRegistry: this.deps.getModelRegistry(),\n\t\t\tcwd: this.deps.getCwd(),\n\t\t});\n\t\tif (!this.deps.isExplicitModel() && profileSettings.model) {\n\t\t\tconst current = this.deps.getAgent().state.model;\n\t\t\tconst next = profileSettings.model;\n\t\t\tif (!current || current.provider !== next.provider || current.id !== next.id) {\n\t\t\t\t// Mirror the startup/cycle path: set the model directly (no auth gate, no settings\n\t\t\t\t// persist) so re-applying the profile model behaves like initial resolution rather\n\t\t\t\t// than a runtime model switch. No model_select emit here — reload rebuilds the\n\t\t\t\t// extension runtime and emits session_start(\"reload\") right after, and the UI\n\t\t\t\t// re-renders from session.model.\n\t\t\t\tthis.deps.getAgent().state.model = next;\n\t\t\t\tthis.deps.getSessionManager().appendModelChange(next.provider, next.id);\n\t\t\t}\n\t\t}\n\t\tif (!this.deps.isExplicitThinking() && profileSettings.thinkingLevel) {\n\t\t\tthis.deps.setThinkingLevel(profileSettings.thinkingLevel);\n\t\t}\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"profile-filter-controller.js","sourceRoot":"","sources":["../../src/core/profile-filter-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAMH,OAAO,EAAE,gBAAgB,EAAE,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AAEpF,OAAO,EACN,6BAA6B,GAG7B,MAAM,uBAAuB,CAAC;AA8B/B,MAAM,OAAO,uBAAuB;IACnC,qFAAqF;IAC7E,uBAAuB,GAAa,EAAE,CAAC;IAC/C,kGAAkG;IAC1F,4BAA4B,GAAG,CAAC,CAAC;IACzC,2GAA2G;IACnG,uBAAuB,GAAG,KAAK,CAAC;IAChC,0BAA0B,GAAG,KAAK,CAAC;IAE1B,IAAI,CAA8B;IAEnD,YAAY,IAAiC,EAAE;QAC9C,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAAA,CACjB;IAED,4FAA4F;IAC5F,yBAAyB,GAAa;QACrC,OAAO,IAAI,CAAC,uBAAuB,CAAC;IAAA,CACpC;IAED,oBAAoB,GAAgC;QACnD,OAAO;YACN,sBAAsB,EAAE,CAAC,GAAG,IAAI,CAAC,uBAAuB,CAAC;YACzD,2BAA2B,EAAE,IAAI,CAAC,4BAA4B;YAC9D,sBAAsB,EAAE,IAAI,CAAC,uBAAuB;YACpD,yBAAyB,EAAE,IAAI,CAAC,0BAA0B;SAC1D,CAAC;IAAA,CACF;IAED,qBAAqB,CAAC,QAAqC,EAAQ;QAClE,IAAI,CAAC,uBAAuB,GAAG,CAAC,GAAG,QAAQ,CAAC,sBAAsB,CAAC,CAAC;QACpE,IAAI,CAAC,4BAA4B,GAAG,QAAQ,CAAC,2BAA2B,CAAC;QACzE,IAAI,CAAC,uBAAuB,GAAG,QAAQ,CAAC,sBAAsB,CAAC;QAC/D,IAAI,CAAC,0BAA0B,GAAG,QAAQ,CAAC,yBAAyB,CAAC;IAAA,CACrE;IAED;;;;OAIG;IACH,uBAAuB,GAA4C;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC;QAChF,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,IAAI,EAAE,EAAE,CAAC;IAAA,CAChE;IAED,+BAA+B,CAAC,IAAY,EAAW;QACtD,MAAM,gBAAgB,GAAG,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,CAAC;QACzD,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC3D,IAAI,gBAAgB,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAClE,IAAI,iBAAiB,EAAE,GAAG,CAAC,IAAI,CAAC;YAAE,OAAO,KAAK,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAChD,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QACzB,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,6BAA6B,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QAChG,IAAI,6BAA6B,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC;QACpE,OAAO,IAAI,CAAC;IAAA,CACZ;IAEO,4BAA4B,GAAY;QAC/C,MAAM,iBAAiB,GAAG,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE,CAAC;QAC3D,OAAO,OAAO,CACb,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE;YAC9B,IAAI,CAAC,IAAI,CAAC,oBAAoB,EAAE;YAChC,CAAC,iBAAiB,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAClG,CAAC;IAAA,CACF;IAED,0BAA0B,CAAC,UAAuB,EAAe;QAChE,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACvD,IAAI,CAAC,uBAAuB,GAAG,EAAE,CAAC;QAClC,IAAI,CAAC,4BAA4B,GAAG,CAAC,CAAC;QACtC,IAAI,eAAe,CAAC,6BAA6B,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAClE,IAAI,eAAe,CAAC,yCAAyC,EAAE,EAAE,CAAC;gBACjE,mFAAmF;gBACnF,2DAAyD;gBACzD,IAAI,CAAC,4BAA4B,GAAG,UAAU,CAAC,MAAM,CAAC;gBACtD,OAAO,EAAE,CAAC;YACX,CAAC;YACD,4FAA4F;YAC5F,sDAAsD;YACtD,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC;QACnF,CAAC;QACD,MAAM,oBAAoB,GAAG,IAAI,CAAC,4BAA4B,EAAE,CAAC;QACjE,MAAM,iBAAiB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,SAAS,EAAE,EAAE,CACzD,eAAe,CAAC,0BAA0B,CAAC,YAAY,EAAE,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,UAAU,CAAC,OAAO,CAAC,CACtG,CAAC;QACF,IAAI,CAAC,4BAA4B,GAAG,UAAU,CAAC,MAAM,GAAG,iBAAiB,CAAC,MAAM,CAAC;QACjF,OAAO,iBAAiB,CAAC,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC;YAC3C,IAAI,CAAC,oBAAoB;gBAAE,OAAO,SAAS,CAAC;YAC5C,MAAM,KAAK,GAAG,IAAI,GAAG,CACpB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,CAAC,CACpG,CAAC;YACF,MAAM,QAAQ,GAAG,IAAI,GAAG,CACvB,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,CAAC,CACvG,CAAC;YACF,qFAAqF;YACrF,uFAAqF;YACrF,4EAA4E;YAC5E,IAAI,SAAS,CAAC,KAAK,CAAC,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBAC5F,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,SAAS,CAAC,IAAI,CAAC;gBACnE,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAChC,cAAc,IAAI,6EAA6E,SAAS,CAAC,KAAK,CAAC,IAAI,gBAAgB,SAAS,CAAC,QAAQ,CAAC,IAAI,4BAA4B,CACtL,CAAC;YACH,CAAC;YACD,OAAO,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;QAAA,CACzC,CAAC,CAAC;IAAA,CACH;IAED;;;;;;;;;;;;;OAaG;IACH,iCAAiC,GAAa;QAC7C,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACvD,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC;QACrD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;QAC/B,MAAM,YAAY,GAAa,EAAE,CAAC;QAClC,MAAM,QAAQ,GAAG,CAAC,IAA0B,EAAE,KAAe,EAAU,EAAE,CACxE,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,eAAe,CAAC,+BAA+B,CAAC,IAAI,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC;QAEjG,MAAM,cAAc,GAAG,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,yBAAyB,EAAE,CAAC,CAAC;QACtF,IAAI,cAAc,GAAG,CAAC,EAAE,CAAC;YACxB,YAAY,CAAC,IAAI,CAChB,GAAG,cAAc,+FAA6F,CAC9G,CAAC;QACH,CAAC;QACD,MAAM,eAAe,GAAG,QAAQ,CAAC,SAAS,EAAE,cAAc,CAAC,0BAA0B,EAAE,CAAC,CAAC;QACzF,IAAI,eAAe,GAAG,CAAC,EAAE,CAAC;YACzB,YAAY,CAAC,IAAI,CAChB,GAAG,eAAe,iGAA+F,CACjH,CAAC;QACH,CAAC;QACD,IAAI,IAAI,CAAC,4BAA4B,GAAG,CAAC,EAAE,CAAC;YAC3C,YAAY,CAAC,IAAI,CAChB,GAAG,IAAI,CAAC,4BAA4B,uGAAqG,CACzI,CAAC;QACH,CAAC;QACD,OAAO,YAAY,CAAC;IAAA,CACpB;IAED;;;;;OAKG;IACH,KAAK,CAAC,iCAAiC,GAAkB;QACxD,IAAI,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE;YAAE,OAAO;QAC1E,MAAM,eAAe,GAAG,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;QACvD,MAAM,kBAAkB,GAAG,eAAe,CAAC,6BAA6B,EAAE,CAAC;QAC3E,MAAM,eAAe,GACpB,kBAAkB,CAAC,MAAM,GAAG,CAAC;YAC5B,CAAC,CAAC,2BAA2B,CAAC;gBAC5B,kBAAkB;gBAClB,QAAQ,EAAE,eAAe,CAAC,kBAAkB,EAAE;gBAC9C,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE;gBAC3C,GAAG,EAAE,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE;aACvB,CAAC;YACH,CAAC,CAAC,EAAE,CAAC;QACP,IAAI,eAAe,CAAC,KAAK,EAAE,CAAC;YAC3B,MAAM,IAAI,KAAK,CAAC,mCAAmC,eAAe,CAAC,KAAK,EAAE,CAAC,CAAC;QAC7E,CAAC;QACD,MAAM,oBAAoB,GACzB,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,IAAI,CAAC,uBAAuB,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC;QACxF,MAAM,uBAAuB,GAC5B,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,IAAI,CAAC,0BAA0B,IAAI,CAAC,eAAe,CAAC,aAAa,CAAC;QACtG,MAAM,aAAa,GAClB,oBAAoB,IAAI,uBAAuB;YAC9C,CAAC,CAAC,MAAM,gBAAgB,CAAC;gBACvB,YAAY,EAAE,EAAE;gBAChB,YAAY,EAAE,IAAI;gBAClB,eAAe,EAAE,eAAe,CAAC,kBAAkB,EAAE;gBACrD,cAAc,EAAE,eAAe,CAAC,eAAe,EAAE;gBACjD,oBAAoB,EAAE,eAAe,CAAC,uBAAuB,EAAE;gBAC/D,aAAa,EAAE,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE;aAC3C,CAAC;YACH,CAAC,CAAC,SAAS,CAAC;QACd,IAAI,YAAY,GAAG,KAAK,CAAC;QACzB,MAAM,SAAS,GAAG,eAAe,CAAC,KAAK,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACrG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,SAAS,EAAE,CAAC;YAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC;YACjD,MAAM,IAAI,GAAG,SAAS,CAAC;YACvB,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,IAAI,OAAO,CAAC,EAAE,KAAK,IAAI,CAAC,EAAE,EAAE,CAAC;gBAC9E,mFAAmF;gBACnF,mFAAmF;gBACnF,iFAA+E;gBAC/E,8EAA8E;gBAC9E,iCAAiC;gBACjC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;gBACxC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;gBACxE,YAAY,GAAG,IAAI,CAAC;YACrB,CAAC;QACF,CAAC;QACD,IAAI,CAAC,uBAAuB,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,IAAI,OAAO,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;QAC9F,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,eAAe,CAAC,aAAa,EAAE,CAAC;YACtE,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;QAC3D,CAAC;aAAM,IAAI,uBAAuB,IAAI,aAAa,EAAE,CAAC;YACrD,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;QACzD,CAAC;aAAM,IAAI,YAAY,EAAE,CAAC;YACzB,sFAAsF;YACtF,sFAAsF;YACtF,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC;QACtE,CAAC;QACD,IAAI,CAAC,0BAA0B,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,IAAI,OAAO,CAAC,eAAe,CAAC,aAAa,CAAC,CAAC;IAAA,CAC5G;CACD","sourcesContent":["/**\n * Resource-profile tool/extension gating + reload-time profile model re-application.\n *\n * Extracted verbatim from agent-session.ts (god-file decomposition). Resolves the active resource\n * profile's tool allow/block filter, filters the runtime extension set (tracking the withheld/inert\n * tallies it owns), reports the /context \"withheld by profile\" observations, and — on reload —\n * re-applies a profile-bound model/thinking level unless an explicit launch flag set them. Owns the\n * inert-extension warnings and profile-denied-extension count; everything else is read through deps.\n */\n\nimport type { Agent, ThinkingLevel } from \"@caupulican/pi-agent-core\";\nimport type { SessionManager } from \"@caupulican/pi-agent-core/node\";\nimport type { Extension } from \"./extensions/index.ts\";\nimport type { ModelRegistry } from \"./model-registry.ts\";\nimport { findInitialModel, resolveProfileModelSettings } from \"./model-resolver.ts\";\nimport type { ResourceLoader } from \"./resource-loader.ts\";\nimport {\n\tmatchesResourceProfilePattern,\n\ttype ResourceProfileFilterSettings,\n\ttype SettingsManager,\n} from \"./settings-manager.ts\";\n\nexport interface ProfileFilterControllerDeps {\n\tgetSettingsManager(): SettingsManager;\n\tgetResourceLoader(): ResourceLoader;\n\tgetModelRegistry(): ModelRegistry;\n\tgetCwd(): string;\n\tgetAgent(): Agent;\n\tgetSessionManager(): SessionManager;\n\t/** Construction-time tool allow-set (undefined = no allow-list gate). */\n\tgetAllowedToolNames(): Set<string> | undefined;\n\t/** Construction-time tool exclude-set. */\n\tgetExcludedToolNames(): Set<string> | undefined;\n\t/** Live resource-profile tool filter (re-derived on reload; may be undefined). */\n\tgetToolProfileFilter(): Required<ResourceProfileFilterSettings> | undefined;\n\t/** Whether the model was set by an explicit launch flag (profile must not clobber it). */\n\tisExplicitModel(): boolean;\n\t/** Whether the thinking level was set by an explicit launch flag. */\n\tisExplicitThinking(): boolean;\n\t/** Apply a thinking level (delegates to model-selection). */\n\tsetThinkingLevel(level: ThinkingLevel): void;\n}\n\nexport interface ProfileFilterReloadSnapshot {\n\tinertExtensionWarnings: string[];\n\tprofileDeniedExtensionCount: number;\n\tprofileControlledModel: boolean;\n\tprofileControlledThinking: boolean;\n}\n\nexport class ProfileFilterController {\n\t/** G12: extensions loaded but rendered fully inert by the profile's tools filter. */\n\tprivate _inertExtensionWarnings: string[] = [];\n\t/** Count of extensions withheld by the active resource profile (for the /context observation). */\n\tprivate _profileDeniedExtensionCount = 0;\n\t/** Whether the last applied profile generation, rather than the base session selection, owns the field. */\n\tprivate _profileControlledModel = false;\n\tprivate _profileControlledThinking = false;\n\n\tprivate readonly deps: ProfileFilterControllerDeps;\n\n\tconstructor(deps: ProfileFilterControllerDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Inert-extension warnings tracked by the last {@link filterExtensionsForRuntime} pass. */\n\tgetInertExtensionWarnings(): string[] {\n\t\treturn this._inertExtensionWarnings;\n\t}\n\n\tcreateReloadSnapshot(): ProfileFilterReloadSnapshot {\n\t\treturn {\n\t\t\tinertExtensionWarnings: [...this._inertExtensionWarnings],\n\t\t\tprofileDeniedExtensionCount: this._profileDeniedExtensionCount,\n\t\t\tprofileControlledModel: this._profileControlledModel,\n\t\t\tprofileControlledThinking: this._profileControlledThinking,\n\t\t};\n\t}\n\n\trestoreReloadSnapshot(snapshot: ProfileFilterReloadSnapshot): void {\n\t\tthis._inertExtensionWarnings = [...snapshot.inertExtensionWarnings];\n\t\tthis._profileDeniedExtensionCount = snapshot.profileDeniedExtensionCount;\n\t\tthis._profileControlledModel = snapshot.profileControlledModel;\n\t\tthis._profileControlledThinking = snapshot.profileControlledThinking;\n\t}\n\n\t/**\n\t * Resolve the active resource-profile tool allow/block filter from current settings.\n\t * Mirrors the construction-time derivation (settingsManager.getResourceProfileFilter(\"tools\"))\n\t * so reload() can re-apply it after a live settings/profile edit.\n\t */\n\tderiveToolProfileFilter(): Required<ResourceProfileFilterSettings> {\n\t\tconst filter = this.deps.getSettingsManager().getResourceProfileFilter(\"tools\");\n\t\treturn { allow: filter.allow ?? [], block: filter.block ?? [] };\n\t}\n\n\tisToolOrCommandAllowedByProfile(name: string): boolean {\n\t\tconst allowedToolNames = this.deps.getAllowedToolNames();\n\t\tconst excludedToolNames = this.deps.getExcludedToolNames();\n\t\tif (allowedToolNames && !allowedToolNames.has(name)) return false;\n\t\tif (excludedToolNames?.has(name)) return false;\n\t\tconst filter = this.deps.getToolProfileFilter();\n\t\tif (!filter) return true;\n\t\tif (filter.allow.length > 0 && !matchesResourceProfilePattern(name, filter.allow)) return false;\n\t\tif (matchesResourceProfilePattern(name, filter.block)) return false;\n\t\treturn true;\n\t}\n\n\tprivate _hasToolOrCommandProfileGate(): boolean {\n\t\tconst toolProfileFilter = this.deps.getToolProfileFilter();\n\t\treturn Boolean(\n\t\t\tthis.deps.getAllowedToolNames() ||\n\t\t\t\tthis.deps.getExcludedToolNames() ||\n\t\t\t\t(toolProfileFilter && (toolProfileFilter.allow.length > 0 || toolProfileFilter.block.length > 0)),\n\t\t);\n\t}\n\n\tfilterExtensionsForRuntime(extensions: Extension[]): Extension[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tthis._inertExtensionWarnings = [];\n\t\tthis._profileDeniedExtensionCount = 0;\n\t\tif (settingsManager.getActiveResourceProfileNames().length === 0) {\n\t\t\tif (settingsManager.hasExplicitActiveResourceProfileSelection()) {\n\t\t\t\t// An explicit profile selection that resolves to no active profile is a deliberate\n\t\t\t\t// deny-all — every extension is withheld by that choice.\n\t\t\t\tthis._profileDeniedExtensionCount = extensions.length;\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\t// No profile in play: only inline/SDK extensions load by default. That is the baseline, not\n\t\t\t// a profile denial, so it is not counted as withheld.\n\t\t\treturn extensions.filter((extension) => extension.sourceInfo.source === \"inline\");\n\t\t}\n\t\tconst hasToolOrCommandGate = this._hasToolOrCommandProfileGate();\n\t\tconst allowedExtensions = extensions.filter((extension) =>\n\t\t\tsettingsManager.isResourceAllowedByProfile(\"extensions\", extension.path, extension.sourceInfo.baseDir),\n\t\t);\n\t\tthis._profileDeniedExtensionCount = extensions.length - allowedExtensions.length;\n\t\treturn allowedExtensions.map((extension) => {\n\t\t\tif (!hasToolOrCommandGate) return extension;\n\t\t\tconst tools = new Map(\n\t\t\t\tArray.from(extension.tools.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\tconst commands = new Map(\n\t\t\t\tArray.from(extension.commands.entries()).filter(([name]) => this.isToolOrCommandAllowedByProfile(name)),\n\t\t\t);\n\t\t\t// G12: an extension the profile ALLOWS whose every tool and command the tools filter\n\t\t\t// then denies loads and runs lifecycle hooks but is completely uninvocable — surface\n\t\t\t// it instead of presenting a \"loaded\" extension that silently does nothing.\n\t\t\tif (extension.tools.size + extension.commands.size > 0 && tools.size + commands.size === 0) {\n\t\t\t\tconst name = extension.path.split(/[\\\\/]/).pop() ?? extension.path;\n\t\t\t\tthis._inertExtensionWarnings.push(\n\t\t\t\t\t`extension \"${name}\" is loaded but fully inert: the active profile's tools filter denies all ${extension.tools.size} tool(s) and ${extension.commands.size} command(s) it contributes`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn { ...extension, tools, commands };\n\t\t});\n\t}\n\n\t/**\n\t * /context observations for skills/prompts/extensions the active resource profile removed from\n\t * listings — the analog of the withheld-AGENTS.md warning. Strict UAC makes these silently absent,\n\t * so a lean profile's effect on the resource surface stays visible. Counts are profile-scoped\n\t * (skills/prompts via the profile-independent discovery universe filtered by the live profile\n\t * filter; extensions via the runtime filter's denied tally). Empty when nothing is withheld.\n\t *\n\t * Uses `isResourceDeniedByActiveProfile` (profile-only), not `isResourceAllowedByProfile` (which\n\t * also folds in the user's own legacy `disabledResources` list): a plain user-disabled resource\n\t * must never be misattributed to \"the active resource profile\" — that case is already surfaced by\n\t * the G14 disable-wins warning. With no active profile at all, the helper always reports nothing\n\t * denied, so this naturally stays silent (extensions keep their own runtime-filter-derived count,\n\t * which is already correctly zero absent a profile).\n\t */\n\tprofileDeniedResourceObservations(): string[] {\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst resourceLoader = this.deps.getResourceLoader();\n\t\tconst cwd = this.deps.getCwd();\n\t\tconst observations: string[] = [];\n\t\tconst withheld = (kind: \"skills\" | \"prompts\", paths: string[]): number =>\n\t\t\tpaths.filter((path) => settingsManager.isResourceDeniedByActiveProfile(kind, path, cwd)).length;\n\n\t\tconst skillsWithheld = withheld(\"skills\", resourceLoader.getDiscoverableSkillPaths());\n\t\tif (skillsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${skillsWithheld} skill(s) withheld by the active resource profile — grant the \"skills\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tconst promptsWithheld = withheld(\"prompts\", resourceLoader.getDiscoverablePromptPaths());\n\t\tif (promptsWithheld > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${promptsWithheld} prompt(s) withheld by the active resource profile — grant the \"prompts\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\tif (this._profileDeniedExtensionCount > 0) {\n\t\t\tobservations.push(\n\t\t\t\t`${this._profileDeniedExtensionCount} extension(s) withheld by the active resource profile — grant the \"extensions\" kind to restore them`,\n\t\t\t);\n\t\t}\n\t\treturn observations;\n\t}\n\n\t/**\n\t * Re-resolve the active resource profile's model/thinking from current settings and apply it.\n\t * Only acts when the profile actually binds model/thinking AND that field was not set by an\n\t * explicit launch flag — so live profile edits apply on reload without clobbering an explicit\n\t * --model/--thinking. A no-op for profiles that don't bind a model.\n\t */\n\tasync reapplyActiveProfileModelSettings(): Promise<void> {\n\t\tif (this.deps.isExplicitModel() && this.deps.isExplicitThinking()) return;\n\t\tconst settingsManager = this.deps.getSettingsManager();\n\t\tconst activeProfileNames = settingsManager.getActiveResourceProfileNames();\n\t\tconst profileSettings =\n\t\t\tactiveProfileNames.length > 0\n\t\t\t\t? resolveProfileModelSettings({\n\t\t\t\t\t\tactiveProfileNames,\n\t\t\t\t\t\tregistry: settingsManager.getProfileRegistry(),\n\t\t\t\t\t\tmodelRegistry: this.deps.getModelRegistry(),\n\t\t\t\t\t\tcwd: this.deps.getCwd(),\n\t\t\t\t\t})\n\t\t\t\t: {};\n\t\tif (profileSettings.error) {\n\t\t\tthrow new Error(`Profile model resolution error: ${profileSettings.error}`);\n\t\t}\n\t\tconst mustRestoreBaseModel =\n\t\t\t!this.deps.isExplicitModel() && this._profileControlledModel && !profileSettings.model;\n\t\tconst mustRestoreBaseThinking =\n\t\t\t!this.deps.isExplicitThinking() && this._profileControlledThinking && !profileSettings.thinkingLevel;\n\t\tconst baseSelection =\n\t\t\tmustRestoreBaseModel || mustRestoreBaseThinking\n\t\t\t\t? await findInitialModel({\n\t\t\t\t\t\tscopedModels: [],\n\t\t\t\t\t\tisContinuing: true,\n\t\t\t\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\t\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\t\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\t\t\t\tmodelRegistry: this.deps.getModelRegistry(),\n\t\t\t\t\t})\n\t\t\t\t: undefined;\n\t\tlet modelChanged = false;\n\t\tconst nextModel = profileSettings.model ?? (mustRestoreBaseModel ? baseSelection?.model : undefined);\n\t\tif (!this.deps.isExplicitModel() && nextModel) {\n\t\t\tconst current = this.deps.getAgent().state.model;\n\t\t\tconst next = nextModel;\n\t\t\tif (!current || current.provider !== next.provider || current.id !== next.id) {\n\t\t\t\t// Mirror the startup/cycle path: set the model directly (no auth gate, no settings\n\t\t\t\t// persist) so re-applying the profile model behaves like initial resolution rather\n\t\t\t\t// than a runtime model switch. No model_select emit here — reload rebuilds the\n\t\t\t\t// extension runtime and emits session_start(\"reload\") right after, and the UI\n\t\t\t\t// re-renders from session.model.\n\t\t\t\tthis.deps.getAgent().state.model = next;\n\t\t\t\tthis.deps.getSessionManager().appendModelChange(next.provider, next.id);\n\t\t\t\tmodelChanged = true;\n\t\t\t}\n\t\t}\n\t\tthis._profileControlledModel = !this.deps.isExplicitModel() && Boolean(profileSettings.model);\n\t\tif (!this.deps.isExplicitThinking() && profileSettings.thinkingLevel) {\n\t\t\tthis.deps.setThinkingLevel(profileSettings.thinkingLevel);\n\t\t} else if (mustRestoreBaseThinking && baseSelection) {\n\t\t\tthis.deps.setThinkingLevel(baseSelection.thinkingLevel);\n\t\t} else if (modelChanged) {\n\t\t\t// A model-only situation switch still has to clamp the inherited/explicit level. This\n\t\t\t// prevents Sol/Terra Ultra from leaking into Luna or another model that stops at Max.\n\t\t\tthis.deps.setThinkingLevel(this.deps.getAgent().state.thinkingLevel);\n\t\t}\n\t\tthis._profileControlledThinking = !this.deps.isExplicitThinking() && Boolean(profileSettings.thinkingLevel);\n\t}\n}\n"]}
|