@dsh-enhanced/assistant-growth-experiments 0.1.12

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-enhanced contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # @dsh-enhanced/assistant-growth-experiments
2
+
3
+ > **显式启用的受监督能力,非默认能力。** 默认个人助理安装不会加载本包。启用时它只组合
4
+ > `@dsh-enhanced/assistant-growth-contract`、Delivery 和 Automations 的当前 Host 协议;没有这些
5
+ > 精确边界会拒绝启动,绝不降级为接受任意 trace 或模型自报。
6
+
7
+ 这是基于可信、版本化工作流证据的持久成长实验控制器:只接受 Delivery 的结构化 trace
8
+ projection,不让原始工具参数离开其 Host 边界;候选经 owner 审批后,依次执行 replay、阻断
9
+ 全部外部副作用的 shadow、至多一次 production canary,并且只有收到 `trusted + achieved` 评估后
10
+ 才使用 Automation CAS 提升。其余路径一律回滚。
11
+
12
+ ## 开发者显式安装
13
+
14
+ ```sh
15
+ dsh plugin --profile web add @dsh-enhanced/assistant-growth-experiments
16
+ dsh --profile web --dump-config
17
+ ```
18
+
19
+ ## 配置
20
+
21
+ ```yaml
22
+ - id: dsh-enhanced-assistant-growth-experiments
23
+ name: '@dsh-enhanced/assistant-growth-experiments'
24
+ inject:
25
+ - assistantAutomations
26
+ - assistantDelivery
27
+ config:
28
+ databasePath: !!js dshHomePath('assistant-growth-experiments/growth.sqlite')
29
+ tickIntervalMs: 5000
30
+ minRepeatedSuccesses: 3
31
+ maxBatchSize: 10
32
+ maxExperimentDurationMs: 604800000
33
+ maxOperationAttempts: 8
34
+ retryBaseMs: 1000
35
+ retryMaxMs: 60000
36
+ ```
37
+
38
+ ## 证据入口与私有 capability
39
+
40
+ Delivery 是唯一的 workflow trace producer。Growth 启动时调用
41
+ `registerWorkflowTraceSink({ contractVersion: 1, sink })`,取得固定
42
+ `sourceId: assistantDelivery`、持久 generation、authority digest 和 disposer。该 sink 是只交给
43
+ 当前注入实例的进程内 capability:Growth 没有 `projectWorkflowTraceRevision` 公共 service 方法,
44
+ 也不会把 sink、registration 或 source attestation 暴露为模型工具。Delivery 或 Growth 卸载、替换
45
+ 或调用 disposer 时会撤权;旧 sink 和旧 generation 不能继续写入。
46
+
47
+ Delivery 先把 revision 写进自己的 durable trace outbox,随后才异步投影并严格校验回执。暂时的
48
+ sink 失败保留原 revision、按退避重试;重载后的有效 registration 会重放当前 revision。每条
49
+ `WorkflowTraceRevision` 都必须携带与该 registration 完全相同的 source attestation,版本允许跳号,
50
+ 但旧版本、同版本不同内容或 digest 不一致都会拒绝。
51
+
52
+ 自动学习只有一个封闭来源:当前 active owner 必须直接回复一条已取得 provider message id 的普通
53
+ Agent reply,发送精确的 `/feedback achieved`;该 reply 的原始入站正文还必须逐字命中 Delivery
54
+ 审查过的静态 catalog selector。selector 不做 trim、大小写折叠、参数解析或实体提取,生成的模板
55
+ 也完全是静态内容。只有这一组合会原子生成 content-free 的 `verified-repetition` trace;`partial`、
56
+ `not-achieved`、未命中 catalog、preview/失败运行、手写 background intent 或无法由 durable
57
+ Inbox/Outbox 重新证明的 reply 只会得到不可改写的本地 no-trace 回执。
58
+
59
+ `/workflow save` 仍是单独的 owner-explicit 路径:它只产生带
60
+ `owner-explicit / deidentification-unproven` attestation 的候选,绝不直接启用 Automation。一个
61
+ owner-explicit trace 可以使候选 ready;自动路径则必须累积至少 `minRepeatedSuccesses` 个不同、当前
62
+ 有效的 `trusted + achieved` taskRef。重复投影同一 taskRef 不会抬高计数。
63
+
64
+ trace source 使用导出的 `workflowArgumentShapeDigest(value)`:它只编码 JSON 类型、对象字段和数组
65
+ 成员类型集合,标量值、数组长度和顺序不会进入 fingerprint。Delivery 将可复用模板内容保留在私有
66
+ registry,只向 Growth 投影 `templateRef/templateDigest/privacyAttestation`;Growth 不保存 raw prompt。
67
+ 这里的可信根是持有私有 sink capability 的 Host 注入 Delivery,而不是 revision 中可伪造的字符串。
68
+
69
+ ## 受监督实验序列
70
+
71
+ Automations 实现导出的 `GrowthAutomationPort`。每个跨库动作先持久化 operation intent,再以固定
72
+ `operationId` 调用;同一 operation 必须精确重放同一 receipt,payload 变化即拒绝。审批只能创建
73
+ `initialState: paused` 的 Automation;通过审批后固定顺序为 replay → effect-blocked shadow → 单次
74
+ production canary → trusted `achieved` 检查 → CAS promotion。canary 启动与查询是两个接口:
75
+ `canaryWorkflowAutomation` 只允许一个固定 exposure operation,pending 状态只能由
76
+ `inspectWorkflowCanary` 查询,不能重新暴露。任一拒绝、过期、证据变化、失败或恢复预算耗尽都会走
77
+ durable rollback,而不是把 paused artifact 留作已启用。
78
+
79
+ Automations 在真正创建 artifact 时才向 Delivery 的私有 resolver 解析 template reference;Growth
80
+ 始终只携带 content-free reference,不能读取或改写 owner prompt、principal 或 delivery binding。
81
+
82
+ ## schema v2 边界
83
+
84
+ 当前账本为 schema v2。v1 是曾保存 prompt-bearing template 的未发布原型,无法在保留该数据的同时
85
+ 满足 content-free 边界,因此**不做就地迁移**:启动会 fail closed。升级者必须先归档或显式删除旧
86
+ v1 数据库,再创建新的 v2 私有账本;不得把旧模板复制进 v2 或伪造 trace 来绕过该边界。
87
+
88
+ 服务名为 `assistantGrowthExperiments`。`health()` 只暴露固定计数和可选错误码,不返回
89
+ workspace、身份、证据内容或数据库路径。
90
+
91
+ ## 权限与数据
92
+
93
+ - 文件系统:在 `databasePath` 创建 mode `0600` 的 SQLite/WAL 账本;父目录为私有目录。
94
+ - 网络:无直接网络访问;跨系统调用只经注入的 Automation/Delivery Host service。
95
+ - 子进程:无。
96
+ - 凭据:不读取、不保存。
97
+ - 浏览器:无。
98
+ - 安装脚本:无。
99
+
100
+ 账本只保存工具 catalog id 和“已脱敏参数结构”的 SHA-256,不接受原始参数值。deadline
101
+ 或尝试预算耗尽后不会提升;已存在 paused/canary artifact 的失败路径进入持久 rollback
102
+ intent。rollback 暂时不可达时保持 fail-closed 并在 health 中报告,而不是伪造终态。
103
+
104
+ ## 兼容性
105
+
106
+ See the repository [compatibility baseline](../../docs/compatibility.md).
@@ -0,0 +1,15 @@
1
+ - insert:
2
+ - id: dsh-enhanced-assistant-growth-experiments
3
+ name: '@dsh-enhanced/assistant-growth-experiments'
4
+ inject:
5
+ - assistantAutomations
6
+ - assistantDelivery
7
+ config:
8
+ databasePath: !!js dshHomePath('assistant-growth-experiments/growth.sqlite')
9
+ tickIntervalMs: 5000
10
+ minRepeatedSuccesses: 3
11
+ maxBatchSize: 10
12
+ maxExperimentDurationMs: 604800000
13
+ maxOperationAttempts: 8
14
+ retryBaseMs: 1000
15
+ retryMaxMs: 60000
package/lib/index.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import { AssistantGrowthExperimentsService, Config, type AssistantGrowthExperimentsConfig } from './service.js';
3
+ import { version } from './version.js';
4
+ export declare const name = "dsh-enhanced-assistant-growth-experiments";
5
+ export declare const inject: string[];
6
+ export { AssistantGrowthExperimentsService, Config, version };
7
+ export type { AssistantGrowthExperimentsConfig };
8
+ export * from './store.js';
9
+ export * from './types.js';
10
+ export declare function apply(ctx: Context, config: AssistantGrowthExperimentsConfig): void;
11
+ export default AssistantGrowthExperimentsService;
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EACL,iCAAiC,EACjC,MAAM,EACN,KAAK,gCAAgC,EACtC,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,eAAO,MAAM,IAAI,8CAA8C,CAAA;AAC/D,eAAO,MAAM,MAAM,UAAgD,CAAA;AACnE,OAAO,EAAE,iCAAiC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;AAC7D,YAAY,EAAE,gCAAgC,EAAE,CAAA;AAChD,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAE1B,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,gCAAgC,GAAG,IAAI,CAElF;AAED,eAAe,iCAAiC,CAAA"}
package/lib/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { AssistantGrowthExperimentsService, Config, } from './service.js';
2
+ import { version } from './version.js';
3
+ export const name = 'dsh-enhanced-assistant-growth-experiments';
4
+ export const inject = ['assistantAutomations', 'assistantDelivery'];
5
+ export { AssistantGrowthExperimentsService, Config, version };
6
+ export * from './store.js';
7
+ export * from './types.js';
8
+ export function apply(ctx, config) {
9
+ new AssistantGrowthExperimentsService(ctx, config);
10
+ }
11
+ export default AssistantGrowthExperimentsService;
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,iCAAiC,EACjC,MAAM,GAEP,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,MAAM,CAAC,MAAM,IAAI,GAAG,2CAA2C,CAAA;AAC/D,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,sBAAsB,EAAE,mBAAmB,CAAC,CAAA;AACnE,OAAO,EAAE,iCAAiC,EAAE,MAAM,EAAE,OAAO,EAAE,CAAA;AAE7D,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAE1B,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,MAAwC;IAC1E,IAAI,iCAAiC,CAAC,GAAG,EAAE,MAAM,CAAC,CAAA;AACpD,CAAC;AAED,eAAe,iCAAiC,CAAA"}
@@ -0,0 +1,53 @@
1
+ import { Context, Service } from '@deepseek-ai/cordis';
2
+ import Schema from '@deepseek-ai/schemastery';
3
+ import { growthPortReceiptDigest } from '@dsh-enhanced/assistant-growth-contract';
4
+ import type { GrowthExperiment, GrowthExperimentConfig, GrowthExperimentHealth, WorkflowCandidate } from './types.js';
5
+ export type AssistantGrowthExperimentsConfig = GrowthExperimentConfig;
6
+ export declare const Config: Schema<GrowthExperimentConfig>;
7
+ export type AssistantGrowthExperimentsErrorCode = 'disposed' | 'invalid-input' | 'service-unavailable';
8
+ export declare class AssistantGrowthExperimentsError extends Error {
9
+ readonly code: AssistantGrowthExperimentsErrorCode;
10
+ constructor(code: AssistantGrowthExperimentsErrorCode, message: string);
11
+ }
12
+ declare module '@deepseek-ai/cordis' {
13
+ interface Context {
14
+ assistantGrowthExperiments: AssistantGrowthExperimentsService;
15
+ }
16
+ }
17
+ export { growthPortReceiptDigest };
18
+ export declare class AssistantGrowthExperimentsService extends Service {
19
+ static Config: Schema<GrowthExperimentConfig>;
20
+ static inject: string[];
21
+ private readonly config;
22
+ private readonly store;
23
+ private readonly automations;
24
+ private readonly now;
25
+ private unregisterTraceSource;
26
+ private timer;
27
+ private flight;
28
+ private rerunRequested;
29
+ private active;
30
+ constructor(ctx: Context, input: GrowthExperimentConfig, options?: {
31
+ now?: () => number;
32
+ });
33
+ health(): GrowthExperimentHealth;
34
+ getCandidate(id: string): WorkflowCandidate | undefined;
35
+ getExperiment(id: string): GrowthExperiment | undefined;
36
+ beginCandidateExperiment(candidateId: string): GrowthExperiment;
37
+ tick(): Promise<void>;
38
+ whenIdle(): Promise<void>;
39
+ private drain;
40
+ private process;
41
+ private candidateFor;
42
+ private artifactRequest;
43
+ private approval;
44
+ private replay;
45
+ private shadow;
46
+ private canary;
47
+ private promotion;
48
+ private rollback;
49
+ private defer;
50
+ private scheduleTick;
51
+ private assertActive;
52
+ }
53
+ //# sourceMappingURL=service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AACtD,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,EACL,uBAAuB,EAUxB,MAAM,yCAAyC,CAAA;AAEhD,OAAO,KAAK,EAIV,gBAAgB,EAChB,sBAAsB,EACtB,sBAAsB,EAItB,iBAAiB,EAElB,MAAM,YAAY,CAAA;AAEnB,MAAM,MAAM,gCAAgC,GAAG,sBAAsB,CAAA;AAErE,eAAO,MAAM,MAAM,EASb,MAAM,CAAC,sBAAsB,CAAC,CAAA;AAIpC,MAAM,MAAM,mCAAmC,GAC3C,UAAU,GACV,eAAe,GACf,qBAAqB,CAAA;AAEzB,qBAAa,+BAAgC,SAAQ,KAAK;IAC5C,QAAQ,CAAC,IAAI,EAAE,mCAAmC;gBAAzC,IAAI,EAAE,mCAAmC,EAAE,OAAO,EAAE,MAAM;CAIhF;AAED,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,OAAO;QAAG,0BAA0B,EAAE,iCAAiC,CAAA;KAAE;CACpF;AAED,OAAO,EAAE,uBAAuB,EAAE,CAAA;AAyClC,qBAAa,iCAAkC,SAAQ,OAAO;IAC5D,MAAM,CAAC,MAAM,iCAAS;IACtB,MAAM,CAAC,MAAM,WAAgD;IAE7D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAkB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAwB;IAC9C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAsB;IAClD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAc;IAClC,OAAO,CAAC,qBAAqB,CAA0B;IACvD,OAAO,CAAC,KAAK,CAA4C;IACzD,OAAO,CAAC,MAAM,CAA2B;IACzC,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,MAAM,CAAO;gBAET,GAAG,EAAE,OAAO,EAAE,KAAK,EAAE,sBAAsB,EAAE,OAAO,GAAE;QAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;KAAO;IAqE7F,MAAM,IAAI,sBAAsB;IAKhC,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,iBAAiB,GAAG,SAAS;IAKvD,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS;IAKvD,wBAAwB,CAAC,WAAW,EAAE,MAAM,GAAG,gBAAgB;IAUzD,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAerB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAIjB,KAAK;YA4BL,OAAO;IAmCrB,OAAO,CAAC,YAAY;IASpB,OAAO,CAAC,eAAe;YAIT,QAAQ;YAiDR,MAAM;YAcN,MAAM;YAcN,MAAM;YA+BN,SAAS;YAgBT,QAAQ;IAStB,OAAO,CAAC,KAAK;IAoBb,OAAO,CAAC,YAAY;IAMpB,OAAO,CAAC,YAAY;CAGrB"}
package/lib/service.js ADDED
@@ -0,0 +1,422 @@
1
+ import { isAbsolute } from 'node:path';
2
+ import { Context, Service } from '@deepseek-ai/cordis';
3
+ import Schema from '@deepseek-ai/schemastery';
4
+ import { growthPortReceiptDigest, validateGrowthAutomationApprovalReceipt, validateGrowthAutomationProposalReceipt, validateGrowthCanaryInspectionReceipt, validateGrowthCanaryReceipt, validateGrowthPromotionReceipt, validateGrowthReplayReceipt, validateGrowthRollbackReceipt, validateGrowthShadowReceipt, validateGrowthWorkflowTraceSourceRegistration, } from '@dsh-enhanced/assistant-growth-contract';
5
+ import { GrowthExperimentsStore, GrowthExperimentsStoreError } from './store.js';
6
+ export const Config = Schema.object({
7
+ databasePath: Schema.string().required(),
8
+ tickIntervalMs: Schema.number().step(1).min(0).max(86_400_000).default(5_000),
9
+ minRepeatedSuccesses: Schema.number().step(1).min(2).max(100).default(3),
10
+ maxBatchSize: Schema.number().step(1).min(1).max(1_000).default(10),
11
+ maxExperimentDurationMs: Schema.number().step(1).min(1_000).max(31_536_000_000).default(604_800_000),
12
+ maxOperationAttempts: Schema.number().step(1).min(1).max(100).default(8),
13
+ retryBaseMs: Schema.number().step(1).min(1).max(86_400_000).default(1_000),
14
+ retryMaxMs: Schema.number().step(1).min(1).max(86_400_000).default(60_000),
15
+ });
16
+ export class AssistantGrowthExperimentsError extends Error {
17
+ code;
18
+ constructor(code, message) {
19
+ super(message);
20
+ this.code = code;
21
+ this.name = 'AssistantGrowthExperimentsError';
22
+ }
23
+ }
24
+ export { growthPortReceiptDigest };
25
+ function identity(experiment) {
26
+ return Object.freeze({
27
+ contractVersion: 1,
28
+ operationId: experiment.operationId,
29
+ experimentId: experiment.id,
30
+ candidateId: experiment.candidateId,
31
+ candidateRevision: experiment.candidateRevision,
32
+ candidateDigest: experiment.candidateDigest,
33
+ });
34
+ }
35
+ function artifact(experiment) {
36
+ if (experiment.artifactId === undefined || experiment.artifactVersion === undefined
37
+ || experiment.artifactDigest === undefined) {
38
+ throw new AssistantGrowthExperimentsError('invalid-input', 'experiment has no paused artifact');
39
+ }
40
+ return Object.freeze({
41
+ artifactId: experiment.artifactId,
42
+ artifactVersion: experiment.artifactVersion,
43
+ artifactDigest: experiment.artifactDigest,
44
+ });
45
+ }
46
+ function requireMethod(value, method, provider) {
47
+ if ((typeof value !== 'object' && typeof value !== 'function') || value === null
48
+ || typeof value[method] !== 'function') {
49
+ throw new AssistantGrowthExperimentsError('service-unavailable', `${provider}.${method} is required`);
50
+ }
51
+ }
52
+ function stableErrorCode(error) {
53
+ const value = ((typeof error === 'object' || typeof error === 'function') && error !== null
54
+ && typeof error.code === 'string')
55
+ ? error.code
56
+ : 'operation-failed';
57
+ const normalized = value.normalize('NFC').trim().toLowerCase();
58
+ return /^[a-z\d][a-z\d.-]{0,63}$/u.test(normalized) ? normalized : 'operation-failed';
59
+ }
60
+ export class AssistantGrowthExperimentsService extends Service {
61
+ static Config = Config;
62
+ static inject = ['assistantAutomations', 'assistantDelivery'];
63
+ config;
64
+ store;
65
+ automations;
66
+ now;
67
+ unregisterTraceSource;
68
+ timer;
69
+ flight;
70
+ rerunRequested = false;
71
+ active = true;
72
+ constructor(ctx, input, options = {}) {
73
+ super(ctx, 'assistantGrowthExperiments');
74
+ let config;
75
+ try {
76
+ config = Config(input);
77
+ }
78
+ catch (error) {
79
+ throw new AssistantGrowthExperimentsError('invalid-input', `invalid configuration: ${String(error)}`);
80
+ }
81
+ if (!isAbsolute(config.databasePath) || config.retryMaxMs < config.retryBaseMs) {
82
+ throw new AssistantGrowthExperimentsError('invalid-input', 'database path or retry bounds are invalid');
83
+ }
84
+ const automations = ctx.get('assistantAutomations');
85
+ for (const method of ['requestWorkflowAutomation', 'settleWorkflowAutomation',
86
+ 'replayWorkflowAutomation', 'shadowWorkflowAutomation', 'canaryWorkflowAutomation',
87
+ 'inspectWorkflowCanary', 'promoteWorkflowAutomation', 'rollbackWorkflowAutomation']) {
88
+ requireMethod(automations, method, 'assistantAutomations');
89
+ }
90
+ const delivery = ctx.get('assistantDelivery');
91
+ requireMethod(delivery, 'registerWorkflowTraceSink', 'assistantDelivery');
92
+ this.config = config;
93
+ this.now = options.now ?? Date.now;
94
+ this.automations = automations;
95
+ this.store = new GrowthExperimentsStore({
96
+ path: config.databasePath,
97
+ minRepeatedSuccesses: config.minRepeatedSuccesses,
98
+ now: this.now,
99
+ });
100
+ let registeredSource;
101
+ try {
102
+ const registration = delivery.registerWorkflowTraceSink({
103
+ contractVersion: 1,
104
+ sink: Object.freeze({
105
+ projectWorkflowTraceRevision: (revision) => {
106
+ if (registeredSource === undefined
107
+ || revision.source.sourceId !== registeredSource.sourceId
108
+ || revision.source.generation !== registeredSource.generation
109
+ || revision.source.authorityDigest !== registeredSource.authorityDigest) {
110
+ throw new AssistantGrowthExperimentsError('invalid-input', 'workflow trace does not match the authenticated Delivery registration');
111
+ }
112
+ const receipt = this.store.projectWorkflowTraceRevision(revision);
113
+ this.scheduleTick();
114
+ return receipt;
115
+ },
116
+ }),
117
+ });
118
+ const validatedRegistration = validateGrowthWorkflowTraceSourceRegistration(registration);
119
+ registeredSource = Object.freeze({ sourceId: validatedRegistration.sourceId,
120
+ generation: validatedRegistration.generation, authorityDigest: validatedRegistration.authorityDigest });
121
+ this.unregisterTraceSource = () => registration.dispose();
122
+ }
123
+ catch (error) {
124
+ this.store.close();
125
+ throw error;
126
+ }
127
+ if (config.tickIntervalMs > 0) {
128
+ this.timer = setInterval(() => { this.scheduleTick(); }, config.tickIntervalMs);
129
+ this.timer.unref?.();
130
+ }
131
+ ctx.effect(() => async () => {
132
+ this.active = false;
133
+ if (this.timer !== undefined)
134
+ clearInterval(this.timer);
135
+ this.unregisterTraceSource?.();
136
+ await this.flight;
137
+ this.store.close();
138
+ }, 'assistant-growth-experiments.runtime');
139
+ }
140
+ health() {
141
+ this.assertActive();
142
+ return this.store.health();
143
+ }
144
+ getCandidate(id) {
145
+ this.assertActive();
146
+ return this.store.getCandidate(id);
147
+ }
148
+ getExperiment(id) {
149
+ this.assertActive();
150
+ return this.store.getExperiment(id);
151
+ }
152
+ beginCandidateExperiment(candidateId) {
153
+ this.assertActive();
154
+ const value = this.store.beginReadyExperiment({
155
+ candidateId,
156
+ maxDurationMs: this.config.maxExperimentDurationMs,
157
+ });
158
+ this.scheduleTick();
159
+ return value;
160
+ }
161
+ async tick() {
162
+ this.assertActive();
163
+ if (this.flight !== undefined) {
164
+ this.rerunRequested = true;
165
+ return this.flight;
166
+ }
167
+ this.flight = (async () => {
168
+ do {
169
+ this.rerunRequested = false;
170
+ await this.drain();
171
+ } while (this.rerunRequested && this.active);
172
+ })().finally(() => { this.flight = undefined; });
173
+ return this.flight;
174
+ }
175
+ async whenIdle() {
176
+ await this.flight;
177
+ }
178
+ async drain() {
179
+ let hadError = false;
180
+ const ready = this.store.listCandidates({ state: 'ready', limit: this.config.maxBatchSize });
181
+ for (const candidate of ready) {
182
+ try {
183
+ this.store.beginReadyExperiment({
184
+ candidateId: candidate.id,
185
+ maxDurationMs: this.config.maxExperimentDurationMs,
186
+ });
187
+ }
188
+ catch (error) {
189
+ if (!(error instanceof GrowthExperimentsStoreError && error.code === 'version-conflict'))
190
+ throw error;
191
+ }
192
+ }
193
+ const rows = this.store.listRunnableExperiments(this.now(), this.config.maxBatchSize);
194
+ for (const row of rows) {
195
+ if (!this.active)
196
+ return;
197
+ try {
198
+ await this.process(row);
199
+ }
200
+ catch (error) {
201
+ if (error instanceof GrowthExperimentsStoreError && error.code === 'version-conflict')
202
+ continue;
203
+ hadError = true;
204
+ this.store.recordError(stableErrorCode(error));
205
+ this.defer(row.id, error);
206
+ }
207
+ }
208
+ if (!hadError)
209
+ this.store.recordError(undefined);
210
+ }
211
+ async process(snapshot) {
212
+ let current = this.store.getExperiment(snapshot.id);
213
+ if (current === undefined || current.version !== snapshot.version)
214
+ return;
215
+ if (current.state === 'approval-pending') {
216
+ if (this.now() > current.deadlineAt) {
217
+ this.store.transitionExperiment({ experimentId: current.id, expectedVersion: current.version,
218
+ expectedState: current.state, state: 'expired', terminalCode: 'deadline-exceeded' });
219
+ return;
220
+ }
221
+ current = this.store.transitionExperiment({
222
+ experimentId: current.id, expectedVersion: current.version, expectedState: current.state,
223
+ state: 'approval-requesting', operationKind: 'approval-settlement',
224
+ operationId: `${current.id}:approval-settlement:${current.version + 1}`,
225
+ ...(current.proposalId === undefined ? {} : { proposalId: current.proposalId }),
226
+ });
227
+ }
228
+ if (this.now() > current.deadlineAt && current.state !== 'approval-requesting'
229
+ && current.state !== 'rollback-pending') {
230
+ this.store.requestRollback({ experimentId: current.id, expectedVersion: current.version,
231
+ code: 'deadline-exceeded' });
232
+ return;
233
+ }
234
+ switch (current.operationKind) {
235
+ case 'approval-proposal':
236
+ case 'approval-settlement':
237
+ await this.approval(current);
238
+ return;
239
+ case 'replay':
240
+ await this.replay(current);
241
+ return;
242
+ case 'shadow':
243
+ await this.shadow(current);
244
+ return;
245
+ case 'canary':
246
+ await this.canary(current, false);
247
+ return;
248
+ case 'canary-inspection':
249
+ await this.canary(current, true);
250
+ return;
251
+ case 'promotion':
252
+ await this.promotion(current);
253
+ return;
254
+ case 'rollback':
255
+ await this.rollback(current);
256
+ return;
257
+ default: throw new AssistantGrowthExperimentsError('invalid-input', 'active experiment lacks a durable operation intent');
258
+ }
259
+ }
260
+ candidateFor(experiment) {
261
+ const value = this.store.getCandidate(experiment.candidateId);
262
+ if (value === undefined || value.revision !== experiment.candidateRevision
263
+ || value.evidenceDigest !== experiment.candidateDigest || value.state !== 'running') {
264
+ throw new GrowthExperimentsStoreError('version-conflict', 'candidate evidence changed');
265
+ }
266
+ return value;
267
+ }
268
+ artifactRequest(experiment) {
269
+ return Object.freeze({ ...identity(experiment), ...artifact(experiment) });
270
+ }
271
+ async approval(experiment) {
272
+ const candidate = this.store.getCandidate(experiment.candidateId);
273
+ const stale = candidate === undefined || candidate.revision !== experiment.candidateRevision
274
+ || candidate.evidenceDigest !== experiment.candidateDigest || candidate.state !== 'running'
275
+ || experiment.terminalCode === 'evidence-superseded';
276
+ const raw = experiment.operationKind === 'approval-proposal'
277
+ ? await this.automations.requestWorkflowAutomation(Object.freeze({
278
+ ...identity(experiment),
279
+ initialState: 'paused',
280
+ scope: experiment.candidateSnapshot.scope,
281
+ ownerBindingId: experiment.candidateSnapshot.ownerBindingId,
282
+ evidenceDigest: experiment.candidateDigest,
283
+ evidenceCount: experiment.candidateSnapshot.evidenceCount,
284
+ template: experiment.candidateSnapshot.template,
285
+ steps: experiment.candidateSnapshot.steps,
286
+ deadlineAt: experiment.deadlineAt,
287
+ }))
288
+ : await this.automations.settleWorkflowAutomation(Object.freeze({
289
+ ...identity(experiment), proposalId: experiment.proposalId,
290
+ }));
291
+ const receipt = experiment.operationKind === 'approval-proposal'
292
+ ? validateGrowthAutomationProposalReceipt(raw, identity(experiment))
293
+ : validateGrowthAutomationApprovalReceipt(raw, identity(experiment));
294
+ if (receipt.outcome === 'approved-paused') {
295
+ const next = stale || this.now() > experiment.deadlineAt ? 'rollback-pending' : 'replay-pending';
296
+ this.store.transitionExperiment({
297
+ experimentId: experiment.id, expectedVersion: experiment.version, expectedState: experiment.state,
298
+ state: next, operationKind: next === 'rollback-pending' ? 'rollback' : 'replay',
299
+ operationId: `${experiment.id}:${next === 'rollback-pending' ? 'rollback' : 'replay'}`,
300
+ proposalId: receipt.proposalId,
301
+ artifact: { id: receipt.artifactId, version: receipt.artifactVersion, digest: receipt.artifactDigest },
302
+ ...(stale ? { terminalCode: 'evidence-superseded' } : {}),
303
+ });
304
+ return;
305
+ }
306
+ if (receipt.outcome === 'approval-pending' && !stale && this.now() <= experiment.deadlineAt) {
307
+ this.store.transitionExperiment({ experimentId: experiment.id, expectedVersion: experiment.version,
308
+ expectedState: experiment.state, state: 'approval-pending', proposalId: receipt.proposalId,
309
+ nextAttemptAt: this.now() + this.config.retryBaseMs });
310
+ return;
311
+ }
312
+ const state = stale ? 'conflicted' : receipt.outcome === 'approval-pending' || receipt.outcome === 'expired'
313
+ ? 'expired' : receipt.outcome;
314
+ this.store.transitionExperiment({ experimentId: experiment.id, expectedVersion: experiment.version,
315
+ expectedState: experiment.state, state,
316
+ ...(receipt.proposalId === undefined ? {} : { proposalId: receipt.proposalId }),
317
+ terminalCode: stale ? 'evidence-superseded' : `approval-${receipt.outcome}` });
318
+ }
319
+ async replay(experiment) {
320
+ this.candidateFor(experiment);
321
+ const request = this.artifactRequest(experiment);
322
+ const receipt = validateGrowthReplayReceipt(await this.automations.replayWorkflowAutomation(request), request);
323
+ this.store.transitionExperiment({ experimentId: experiment.id, expectedVersion: experiment.version,
324
+ expectedState: experiment.state, state: receipt.outcome === 'passed' ? 'shadow-pending' : 'rollback-pending',
325
+ operationKind: receipt.outcome === 'passed' ? 'shadow' : 'rollback',
326
+ operationId: `${experiment.id}:${receipt.outcome === 'passed' ? 'shadow' : 'rollback'}`,
327
+ ...(receipt.outcome === 'failed' ? { terminalCode: 'replay-failed' } : {}),
328
+ });
329
+ }
330
+ async shadow(experiment) {
331
+ this.candidateFor(experiment);
332
+ const request = this.artifactRequest(experiment);
333
+ const receipt = validateGrowthShadowReceipt(await this.automations.shadowWorkflowAutomation(request), request);
334
+ this.store.transitionExperiment({ experimentId: experiment.id, expectedVersion: experiment.version,
335
+ expectedState: experiment.state, state: receipt.outcome === 'passed' ? 'canary-pending' : 'rollback-pending',
336
+ operationKind: receipt.outcome === 'passed' ? 'canary' : 'rollback',
337
+ operationId: `${experiment.id}:${receipt.outcome === 'passed' ? 'canary' : 'rollback'}`,
338
+ ...(receipt.outcome === 'failed' ? { terminalCode: 'shadow-failed' } : {}),
339
+ });
340
+ }
341
+ async canary(experiment, inspection) {
342
+ this.candidateFor(experiment);
343
+ let current = experiment;
344
+ if (!inspection && current.canaryExposureCount === 0) {
345
+ current = this.store.markCanaryIssued({ experimentId: current.id, expectedVersion: current.version });
346
+ }
347
+ const request = this.artifactRequest(current);
348
+ const inspectionRequest = Object.freeze({
349
+ ...request, exposureOperationId: `${current.id}:canary`,
350
+ });
351
+ const raw = inspection
352
+ ? await this.automations.inspectWorkflowCanary(inspectionRequest)
353
+ : await this.automations.canaryWorkflowAutomation(request);
354
+ const receipt = inspection
355
+ ? validateGrowthCanaryInspectionReceipt(raw, inspectionRequest)
356
+ : validateGrowthCanaryReceipt(raw, request);
357
+ if (receipt.outcome === 'pending') {
358
+ this.store.transitionExperiment({ experimentId: current.id, expectedVersion: current.version,
359
+ expectedState: current.state, state: 'canary-pending', operationKind: 'canary-inspection',
360
+ operationId: `${current.id}:canary-inspection:${current.version + 1}`,
361
+ canaryExposureCount: 1, nextAttemptAt: this.now() + this.config.retryBaseMs });
362
+ return;
363
+ }
364
+ this.store.transitionExperiment({ experimentId: current.id, expectedVersion: current.version,
365
+ expectedState: current.state, state: receipt.outcome === 'passed' ? 'promotion-pending' : 'rollback-pending',
366
+ operationKind: receipt.outcome === 'passed' ? 'promotion' : 'rollback',
367
+ operationId: `${current.id}:${receipt.outcome === 'passed' ? 'promotion' : 'rollback'}`,
368
+ ...(receipt.outcome === 'failed' ? { terminalCode: 'canary-failed' } : {}),
369
+ });
370
+ }
371
+ async promotion(experiment) {
372
+ this.candidateFor(experiment);
373
+ const request = this.artifactRequest(experiment);
374
+ const receipt = validateGrowthPromotionReceipt(await this.automations.promoteWorkflowAutomation(request), request);
375
+ this.store.transitionExperiment({ experimentId: experiment.id, expectedVersion: experiment.version,
376
+ expectedState: experiment.state, state: 'promoted', terminalCode: 'trusted-canary-promoted',
377
+ artifact: {
378
+ id: experiment.artifactId,
379
+ version: receipt.resultingArtifactVersion,
380
+ digest: receipt.resultingArtifactDigest,
381
+ },
382
+ });
383
+ }
384
+ async rollback(experiment) {
385
+ const request = this.artifactRequest(experiment);
386
+ validateGrowthRollbackReceipt(await this.automations.rollbackWorkflowAutomation(request), request);
387
+ this.store.transitionExperiment({ experimentId: experiment.id, expectedVersion: experiment.version,
388
+ expectedState: experiment.state, state: 'rolled-back', terminalCode: experiment.terminalCode ?? 'rolled-back' });
389
+ }
390
+ defer(experimentId, error) {
391
+ const current = this.store.getExperiment(experimentId);
392
+ if (current === undefined || ['conflicted', 'expired', 'promoted', 'rejected', 'rolled-back']
393
+ .includes(current.state))
394
+ return;
395
+ const attempts = current.attemptCount + 1;
396
+ if (attempts >= this.config.maxOperationAttempts && current.state !== 'approval-requesting') {
397
+ if (current.state === 'rollback-pending') {
398
+ this.store.recordOperationFailure({ experimentId: current.id, expectedVersion: current.version,
399
+ code: 'rollback-retry-budget-exhausted', nextAttemptAt: this.now() + this.config.retryMaxMs });
400
+ }
401
+ else {
402
+ this.store.requestRollback({ experimentId: current.id, expectedVersion: current.version,
403
+ code: 'operation-retry-budget-exhausted' });
404
+ }
405
+ return;
406
+ }
407
+ const delay = Math.min(this.config.retryMaxMs, this.config.retryBaseMs * (2 ** Math.min(attempts - 1, 20)));
408
+ this.store.recordOperationFailure({ experimentId: current.id, expectedVersion: current.version,
409
+ code: stableErrorCode(error), nextAttemptAt: this.now() + delay });
410
+ }
411
+ scheduleTick() {
412
+ void this.tick().catch(error => {
413
+ if (this.active)
414
+ this.store.recordError(stableErrorCode(error));
415
+ });
416
+ }
417
+ assertActive() {
418
+ if (!this.active)
419
+ throw new AssistantGrowthExperimentsError('disposed', 'service is disposed');
420
+ }
421
+ }
422
+ //# sourceMappingURL=service.js.map