@haaaiawd/second-nature 0.1.26 → 0.1.29
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/SKILL.md +35 -0
- package/agent-inner-guide.md +144 -0
- package/index.js +280 -2
- package/openclaw.plugin.json +2 -2
- package/package.json +4 -1
- package/runtime/cli/commands/connector-behavior.d.ts +20 -0
- package/runtime/cli/commands/connector-behavior.js +160 -0
- package/runtime/cli/commands/index.js +8 -0
- package/runtime/cli/index.js +9 -2
- package/runtime/cli/ops/manual-run-dispatcher.d.ts +79 -0
- package/runtime/cli/ops/manual-run-dispatcher.js +110 -0
- package/runtime/cli/ops/ops-router.d.ts +45 -4
- package/runtime/cli/ops/ops-router.js +543 -2
- package/runtime/cli/read-models/index.js +35 -18
- package/runtime/cli/read-models/types.d.ts +1 -0
- package/runtime/connectors/agent-network/agent-world/adapter.d.ts +1 -0
- package/runtime/connectors/agent-network/agent-world/adapter.js +2 -2
- package/runtime/connectors/base/contract.d.ts +4 -1
- package/runtime/connectors/base/contract.js +5 -1
- package/runtime/connectors/base/effect-commit-ledger-sqlite.d.ts +31 -0
- package/runtime/connectors/base/effect-commit-ledger-sqlite.js +86 -0
- package/runtime/connectors/base/failure-taxonomy.js +5 -0
- package/runtime/connectors/base/manifest-v7.d.ts +151 -0
- package/runtime/connectors/base/manifest-v7.js +170 -0
- package/runtime/connectors/base/manifest.d.ts +3 -13
- package/runtime/connectors/base/manifest.js +7 -7
- package/runtime/connectors/base/route-planner.js +11 -8
- package/runtime/connectors/base/structured-unavailable-reason.d.ts +59 -0
- package/runtime/connectors/base/structured-unavailable-reason.js +113 -0
- package/runtime/connectors/base/wet-probe-runner.d.ts +40 -0
- package/runtime/connectors/base/wet-probe-runner.js +132 -0
- package/runtime/connectors/manifest/manifest-schema.d.ts +4 -0
- package/runtime/connectors/manifest/manifest-schema.js +2 -0
- package/runtime/connectors/services/connector-executor-adapter.d.ts +1 -0
- package/runtime/connectors/services/connector-executor-adapter.js +132 -26
- package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.d.ts +45 -0
- package/runtime/core/second-nature/body/behavior-promotion/behavior-promotion-loop.js +132 -0
- package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.d.ts +60 -0
- package/runtime/core/second-nature/body/circuit-breaker/circuit-breaker-manager.js +174 -0
- package/runtime/core/second-nature/body/probe-signal-adapter.d.ts +38 -0
- package/runtime/core/second-nature/body/probe-signal-adapter.js +60 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.d.ts +51 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-assembler.js +129 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.d.ts +30 -0
- package/runtime/core/second-nature/body/tool-affordance/affordance-context-scope.js +92 -0
- package/runtime/core/second-nature/body/tool-experience/experience-writer.d.ts +34 -0
- package/runtime/core/second-nature/body/tool-experience/experience-writer.js +67 -0
- package/runtime/core/second-nature/body/tool-experience/pain-signal-query.d.ts +37 -0
- package/runtime/core/second-nature/body/tool-experience/pain-signal-query.js +62 -0
- package/runtime/core/second-nature/heartbeat/decision-trace-emitter.d.ts +29 -0
- package/runtime/core/second-nature/heartbeat/decision-trace-emitter.js +28 -0
- package/runtime/core/second-nature/heartbeat/embodied-context-assembler.d.ts +54 -0
- package/runtime/core/second-nature/heartbeat/embodied-context-assembler.js +164 -0
- package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.d.ts +37 -0
- package/runtime/core/second-nature/heartbeat/goal-lifecycle-policy.js +61 -0
- package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.d.ts +37 -0
- package/runtime/core/second-nature/heartbeat/idle-curiosity-policy.js +60 -0
- package/runtime/core/second-nature/heartbeat/index.d.ts +4 -0
- package/runtime/core/second-nature/heartbeat/index.js +5 -0
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.d.ts +63 -0
- package/runtime/core/second-nature/heartbeat/run-heartbeat-cycle-v7.js +118 -0
- package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.d.ts +41 -0
- package/runtime/core/second-nature/orchestrator/downstream-intent-orchestrator.js +43 -0
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.d.ts +2 -1
- package/runtime/core/second-nature/orchestrator/effect-dispatcher.js +2 -0
- package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.d.ts +31 -0
- package/runtime/core/second-nature/orchestrator/hard-guard-evaluator.js +102 -0
- package/runtime/core/second-nature/orchestrator/index.d.ts +5 -0
- package/runtime/core/second-nature/orchestrator/index.js +7 -0
- package/runtime/core/second-nature/quiet/claim-synthesizer.d.ts +53 -0
- package/runtime/core/second-nature/quiet/claim-synthesizer.js +153 -0
- package/runtime/core/second-nature/quiet/daily-diary-writer.d.ts +29 -0
- package/runtime/core/second-nature/quiet/daily-diary-writer.js +92 -0
- package/runtime/core/second-nature/quiet/index.d.ts +5 -0
- package/runtime/core/second-nature/quiet/index.js +5 -0
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +19 -12
- package/runtime/core/second-nature/types.d.ts +2 -0
- package/runtime/guidance/channel-feedback-ingestion-service.d.ts +88 -0
- package/runtime/guidance/channel-feedback-ingestion-service.js +231 -0
- package/runtime/guidance/guidance-draft-service.d.ts +60 -0
- package/runtime/guidance/guidance-draft-service.js +80 -0
- package/runtime/guidance/index.d.ts +3 -0
- package/runtime/guidance/index.js +3 -0
- package/runtime/guidance/outreach-draft-schema.d.ts +8 -8
- package/runtime/guidance/outreach-strategy-selector.d.ts +77 -0
- package/runtime/guidance/outreach-strategy-selector.js +211 -0
- package/runtime/observability/audit/append-only-audit-store.d.ts +20 -2
- package/runtime/observability/audit/append-only-audit-store.js +32 -6
- package/runtime/observability/audit/audit-envelope.d.ts +2 -1
- package/runtime/observability/audit/audit-envelope.js +8 -7
- package/runtime/observability/audit/audit-family-registry.json +66 -0
- package/runtime/observability/audit/family-registry.d.ts +43 -0
- package/runtime/observability/audit/family-registry.js +70 -0
- package/runtime/observability/index.d.ts +6 -1
- package/runtime/observability/index.js +6 -1
- package/runtime/observability/redaction/policy.d.ts +24 -3
- package/runtime/observability/redaction/policy.js +74 -0
- package/runtime/observability/services/heartbeat-digest-assembler.d.ts +152 -0
- package/runtime/observability/services/heartbeat-digest-assembler.js +248 -0
- package/runtime/observability/services/lived-experience-audit.js +6 -6
- package/runtime/observability/services/narrative-timeline-query-service.d.ts +136 -0
- package/runtime/observability/services/narrative-timeline-query-service.js +169 -0
- package/runtime/observability/services/restore-audit-service.d.ts +74 -0
- package/runtime/observability/services/restore-audit-service.js +79 -0
- package/runtime/observability/services/runtime-secret-anchor-view.d.ts +77 -0
- package/runtime/observability/services/runtime-secret-anchor-view.js +168 -0
- package/runtime/observability/services/self-health-snapshot.d.ts +92 -0
- package/runtime/observability/services/self-health-snapshot.js +251 -0
- package/runtime/shared/types/goal.d.ts +62 -0
- package/runtime/shared/types/goal.js +20 -0
- package/runtime/shared/types/index.d.ts +3 -0
- package/runtime/shared/types/index.js +3 -0
- package/runtime/shared/types/source-ref.d.ts +14 -0
- package/runtime/shared/types/source-ref.js +1 -0
- package/runtime/shared/types/v7-entities.d.ts +206 -0
- package/runtime/shared/types/v7-entities.js +27 -0
- package/runtime/storage/db/index.js +3 -0
- package/runtime/storage/db/migration-runner.d.ts +30 -0
- package/runtime/storage/db/migration-runner.js +93 -0
- package/runtime/storage/db/migrations/index.d.ts +5 -0
- package/runtime/storage/db/migrations/index.js +13 -0
- package/runtime/storage/db/migrations/v7-001-foundation.d.ts +13 -0
- package/runtime/storage/db/migrations/v7-001-foundation.js +144 -0
- package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.d.ts +8 -0
- package/runtime/storage/db/migrations/v7-002-effect-commit-ledger.js +27 -0
- package/runtime/storage/db/migrations/v7-003-circuit-breaker.d.ts +7 -0
- package/runtime/storage/db/migrations/v7-003-circuit-breaker.js +26 -0
- package/runtime/storage/db/migrations/v7-004-behavior-promotion.d.ts +7 -0
- package/runtime/storage/db/migrations/v7-004-behavior-promotion.js +26 -0
- package/runtime/storage/db/schema/agent-goal.d.ts +38 -0
- package/runtime/storage/db/schema/agent-goal.js +2 -0
- package/runtime/storage/db/transaction-utils.d.ts +14 -0
- package/runtime/storage/db/transaction-utils.js +29 -0
- package/runtime/storage/db/write-queue.d.ts +38 -0
- package/runtime/storage/db/write-queue.js +97 -0
- package/runtime/storage/quiet/persist-quiet-artifact.js +2 -1
- package/runtime/storage/services/credential-vault.js +31 -17
- package/runtime/storage/services/diary-dream-store.d.ts +35 -0
- package/runtime/storage/services/diary-dream-store.js +165 -0
- package/runtime/storage/services/embodied-context-state-port.d.ts +77 -0
- package/runtime/storage/services/embodied-context-state-port.js +115 -0
- package/runtime/storage/services/goal-lifecycle-store.d.ts +42 -0
- package/runtime/storage/services/goal-lifecycle-store.js +181 -0
- package/runtime/storage/services/history-digest-store.d.ts +33 -0
- package/runtime/storage/services/history-digest-store.js +140 -0
- package/runtime/storage/services/identity-profile-store.d.ts +25 -0
- package/runtime/storage/services/identity-profile-store.js +81 -0
- package/runtime/storage/services/interaction-snapshot-projector.d.ts +15 -0
- package/runtime/storage/services/interaction-snapshot-projector.js +35 -0
- package/runtime/storage/services/restore-snapshot-store.d.ts +52 -0
- package/runtime/storage/services/restore-snapshot-store.js +193 -0
- package/runtime/storage/services/runtime-secret-anchor-store.d.ts +26 -0
- package/runtime/storage/services/runtime-secret-anchor-store.js +82 -0
- package/runtime/storage/services/tool-experience-store.d.ts +25 -0
- package/runtime/storage/services/tool-experience-store.js +116 -0
- package/runtime/storage/services/write-validation-gate.d.ts +46 -0
- package/runtime/storage/services/write-validation-gate.js +200 -0
- package/workspace-ops-bridge.js +16 -1
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BehaviorPromotionLoop — T-BTS.C.3
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Operator-authorized behavior suggestion lifecycle.
|
|
5
|
+
* - candidate → approved (idempotent: repeated approve returns existing)
|
|
6
|
+
* - candidate → rejected (with reason)
|
|
7
|
+
* - candidate → expired (7 days TTL from submission)
|
|
8
|
+
* - rejected/expired are read-only; new submit creates a fresh candidate
|
|
9
|
+
*
|
|
10
|
+
* Dependencies:
|
|
11
|
+
* - `StateDatabase` from `../../../../storage/db/index.js`
|
|
12
|
+
*
|
|
13
|
+
* Boundary:
|
|
14
|
+
* - Does NOT grant execution authorization; approval is a bookkeeping signal.
|
|
15
|
+
* - Only accepts operator-authorized suggestions, not connector auto-probe results.
|
|
16
|
+
*
|
|
17
|
+
* Test coverage: tests/unit/body/behavior-promotion-loop.test.ts
|
|
18
|
+
*/
|
|
19
|
+
import type { StateDatabase } from "../../../../storage/db/index.js";
|
|
20
|
+
export type PromotionStatus = "candidate" | "approved" | "rejected" | "expired";
|
|
21
|
+
export interface BehaviorPromotion {
|
|
22
|
+
promotionId: string;
|
|
23
|
+
behaviorKind: string;
|
|
24
|
+
description: string;
|
|
25
|
+
status: PromotionStatus;
|
|
26
|
+
operatorId?: string;
|
|
27
|
+
rejectReason?: string;
|
|
28
|
+
submittedAt: string;
|
|
29
|
+
decidedAt?: string;
|
|
30
|
+
expiresAt: string;
|
|
31
|
+
}
|
|
32
|
+
export interface BehaviorPromotionLoop {
|
|
33
|
+
submitPromotion(input: {
|
|
34
|
+
promotionId: string;
|
|
35
|
+
behaviorKind: string;
|
|
36
|
+
description: string;
|
|
37
|
+
operatorId?: string;
|
|
38
|
+
}): Promise<BehaviorPromotion>;
|
|
39
|
+
approvePromotion(promotionId: string): Promise<BehaviorPromotion>;
|
|
40
|
+
rejectPromotion(promotionId: string, reason: string): Promise<BehaviorPromotion>;
|
|
41
|
+
loadPromotion(promotionId: string): Promise<BehaviorPromotion | undefined>;
|
|
42
|
+
listPromotions(status?: PromotionStatus): Promise<BehaviorPromotion[]>;
|
|
43
|
+
expireStaleCandidates(): Promise<number>;
|
|
44
|
+
}
|
|
45
|
+
export declare function createBehaviorPromotionLoop(database: StateDatabase): BehaviorPromotionLoop;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BehaviorPromotionLoop — T-BTS.C.3
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Operator-authorized behavior suggestion lifecycle.
|
|
5
|
+
* - candidate → approved (idempotent: repeated approve returns existing)
|
|
6
|
+
* - candidate → rejected (with reason)
|
|
7
|
+
* - candidate → expired (7 days TTL from submission)
|
|
8
|
+
* - rejected/expired are read-only; new submit creates a fresh candidate
|
|
9
|
+
*
|
|
10
|
+
* Dependencies:
|
|
11
|
+
* - `StateDatabase` from `../../../../storage/db/index.js`
|
|
12
|
+
*
|
|
13
|
+
* Boundary:
|
|
14
|
+
* - Does NOT grant execution authorization; approval is a bookkeeping signal.
|
|
15
|
+
* - Only accepts operator-authorized suggestions, not connector auto-probe results.
|
|
16
|
+
*
|
|
17
|
+
* Test coverage: tests/unit/body/behavior-promotion-loop.test.ts
|
|
18
|
+
*/
|
|
19
|
+
const DEFAULT_TTL_DAYS = 7;
|
|
20
|
+
function rowToPromotion(row, cols) {
|
|
21
|
+
const get = (name) => row[cols.indexOf(name)];
|
|
22
|
+
return {
|
|
23
|
+
promotionId: get("promotion_id"),
|
|
24
|
+
behaviorKind: get("behavior_kind"),
|
|
25
|
+
description: get("description"),
|
|
26
|
+
status: get("status"),
|
|
27
|
+
operatorId: get("operator_id") ?? undefined,
|
|
28
|
+
rejectReason: get("reject_reason") ?? undefined,
|
|
29
|
+
submittedAt: get("submitted_at"),
|
|
30
|
+
decidedAt: get("decided_at") ?? undefined,
|
|
31
|
+
expiresAt: get("expires_at"),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function createBehaviorPromotionLoop(database) {
|
|
35
|
+
const { sqlite } = database;
|
|
36
|
+
function loadRecord(promotionId) {
|
|
37
|
+
const result = sqlite.exec(`SELECT * FROM behavior_promotion WHERE promotion_id = ?`, [promotionId]);
|
|
38
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return rowToPromotion(result[0].values[0], result[0].columns);
|
|
42
|
+
}
|
|
43
|
+
function saveStatus(promotionId, status, decidedAt, rejectReason) {
|
|
44
|
+
sqlite.run(`UPDATE behavior_promotion
|
|
45
|
+
SET status = ?, decided_at = ?, reject_reason = ?
|
|
46
|
+
WHERE promotion_id = ?`, [status, decidedAt, rejectReason ?? null, promotionId]);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
async submitPromotion(input) {
|
|
50
|
+
const now = new Date().toISOString();
|
|
51
|
+
const expiresAt = new Date(Date.now() + DEFAULT_TTL_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
52
|
+
sqlite.run(`INSERT INTO behavior_promotion
|
|
53
|
+
(promotion_id, behavior_kind, description, status,
|
|
54
|
+
operator_id, submitted_at, expires_at)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, [
|
|
56
|
+
input.promotionId,
|
|
57
|
+
input.behaviorKind,
|
|
58
|
+
input.description,
|
|
59
|
+
"candidate",
|
|
60
|
+
input.operatorId ?? null,
|
|
61
|
+
now,
|
|
62
|
+
expiresAt,
|
|
63
|
+
]);
|
|
64
|
+
return {
|
|
65
|
+
promotionId: input.promotionId,
|
|
66
|
+
behaviorKind: input.behaviorKind,
|
|
67
|
+
description: input.description,
|
|
68
|
+
status: "candidate",
|
|
69
|
+
operatorId: input.operatorId,
|
|
70
|
+
submittedAt: now,
|
|
71
|
+
expiresAt,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
async approvePromotion(promotionId) {
|
|
75
|
+
const rec = loadRecord(promotionId);
|
|
76
|
+
if (!rec) {
|
|
77
|
+
throw new Error(`promotion_not_found:${promotionId}`);
|
|
78
|
+
}
|
|
79
|
+
if (rec.status === "approved") {
|
|
80
|
+
return rec; // idempotent
|
|
81
|
+
}
|
|
82
|
+
if (rec.status === "rejected" || rec.status === "expired") {
|
|
83
|
+
throw new Error(`promotion_immutable:${promotionId}:${rec.status}`);
|
|
84
|
+
}
|
|
85
|
+
const now = new Date().toISOString();
|
|
86
|
+
saveStatus(promotionId, "approved", now);
|
|
87
|
+
return { ...rec, status: "approved", decidedAt: now };
|
|
88
|
+
},
|
|
89
|
+
async rejectPromotion(promotionId, reason) {
|
|
90
|
+
const rec = loadRecord(promotionId);
|
|
91
|
+
if (!rec) {
|
|
92
|
+
throw new Error(`promotion_not_found:${promotionId}`);
|
|
93
|
+
}
|
|
94
|
+
if (rec.status === "rejected") {
|
|
95
|
+
return rec; // idempotent
|
|
96
|
+
}
|
|
97
|
+
if (rec.status === "approved" || rec.status === "expired") {
|
|
98
|
+
throw new Error(`promotion_immutable:${promotionId}:${rec.status}`);
|
|
99
|
+
}
|
|
100
|
+
const now = new Date().toISOString();
|
|
101
|
+
saveStatus(promotionId, "rejected", now, reason);
|
|
102
|
+
return { ...rec, status: "rejected", decidedAt: now, rejectReason: reason };
|
|
103
|
+
},
|
|
104
|
+
async loadPromotion(promotionId) {
|
|
105
|
+
return loadRecord(promotionId);
|
|
106
|
+
},
|
|
107
|
+
async listPromotions(status) {
|
|
108
|
+
let sql = `SELECT * FROM behavior_promotion`;
|
|
109
|
+
const params = [];
|
|
110
|
+
if (status) {
|
|
111
|
+
sql += ` WHERE status = ?`;
|
|
112
|
+
params.push(status);
|
|
113
|
+
}
|
|
114
|
+
sql += ` ORDER BY submitted_at DESC`;
|
|
115
|
+
const result = sqlite.exec(sql, params);
|
|
116
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
return result[0].values.map((row) => rowToPromotion(row, result[0].columns));
|
|
120
|
+
},
|
|
121
|
+
async expireStaleCandidates() {
|
|
122
|
+
const now = new Date().toISOString();
|
|
123
|
+
sqlite.run(`UPDATE behavior_promotion
|
|
124
|
+
SET status = 'expired', decided_at = ?
|
|
125
|
+
WHERE status = 'candidate' AND expires_at < ?`, [now, now]);
|
|
126
|
+
// sql.js does not provide changes count easily; approximate via re-query
|
|
127
|
+
const result = sqlite.exec(`SELECT COUNT(*) as cnt FROM behavior_promotion
|
|
128
|
+
WHERE status = 'expired' AND decided_at = ?`, [now]);
|
|
129
|
+
return result[0]?.values[0]?.[0] ?? 0;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CircuitBreakerManager — T-BTS.C.5
|
|
3
|
+
*
|
|
4
|
+
* Core logic: State machine (Closed → Open → HalfOpen → Closed/Open).
|
|
5
|
+
* - Closed: counts consecutive failures; threshold hit → Open.
|
|
6
|
+
* - Open: rejects execution; cooldown elapsed → HalfOpen.
|
|
7
|
+
* - HalfOpen: initiates runWetProbe via ProbeSignalAdapter.
|
|
8
|
+
* - strict side-effect → probe_policy_denied, stays HalfOpen.
|
|
9
|
+
* - probe success → Closed + invalidate affordance cache (DR-003).
|
|
10
|
+
* - probe failure → Open.
|
|
11
|
+
*
|
|
12
|
+
* Persistence:
|
|
13
|
+
* - State stored in SQLite `circuit_breaker_state` table (v7-003).
|
|
14
|
+
* - Loads previous state on first access.
|
|
15
|
+
*
|
|
16
|
+
* Dependencies:
|
|
17
|
+
* - `StateDatabase` from `../../../../storage/db/index.js`
|
|
18
|
+
* - `WetProbeRunner` from `../../../../connectors/base/wet-probe-runner.js`
|
|
19
|
+
* - `CapabilityContractRegistryV7` from `../../../../connectors/base/manifest-v7.js`
|
|
20
|
+
* - `ProbeSignalAdapter` from `../probe-signal-adapter.js`
|
|
21
|
+
*
|
|
22
|
+
* Boundary:
|
|
23
|
+
* - Manager decides WHEN to probe (DR-002); connector-system executes it.
|
|
24
|
+
* - Does NOT execute HTTP directly — delegates to ProbeSignalAdapter.
|
|
25
|
+
*
|
|
26
|
+
* Test coverage: tests/unit/body/circuit-breaker-manager.test.ts
|
|
27
|
+
*/
|
|
28
|
+
import type { StateDatabase } from "../../../../storage/db/index.js";
|
|
29
|
+
import type { CapabilityContractRegistryV7 } from "../../../../connectors/base/manifest-v7.js";
|
|
30
|
+
import type { ProbeSignalAdapter } from "../probe-signal-adapter.js";
|
|
31
|
+
export type BreakerState = "closed" | "open" | "half_open";
|
|
32
|
+
export interface BreakerRecord {
|
|
33
|
+
platformId: string;
|
|
34
|
+
capabilityId: string;
|
|
35
|
+
state: BreakerState;
|
|
36
|
+
failureCount: number;
|
|
37
|
+
consecutiveFailures: number;
|
|
38
|
+
lastFailureAt?: string;
|
|
39
|
+
openedAt?: string;
|
|
40
|
+
lastProbeAt?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface CircuitBreakerManager {
|
|
43
|
+
evaluateFailure(platformId: string, capabilityId: string): Promise<BreakerState>;
|
|
44
|
+
evaluateSuccess(platformId: string, capabilityId: string): Promise<BreakerState>;
|
|
45
|
+
canExecute(platformId: string, capabilityId: string): Promise<boolean>;
|
|
46
|
+
getState(platformId: string, capabilityId: string): Promise<BreakerState>;
|
|
47
|
+
attemptReset(platformId: string, capabilityId: string): Promise<BreakerState>;
|
|
48
|
+
}
|
|
49
|
+
export interface CircuitBreakerManagerOptions {
|
|
50
|
+
database: StateDatabase;
|
|
51
|
+
probeAdapter: ProbeSignalAdapter;
|
|
52
|
+
registry: CapabilityContractRegistryV7;
|
|
53
|
+
/** Consecutive failures before opening. Default 3. */
|
|
54
|
+
failureThreshold?: number;
|
|
55
|
+
/** Cooldown in ms before HalfOpen. Default 30_000. */
|
|
56
|
+
cooldownMs?: number;
|
|
57
|
+
/** Callback when breaker transitions to Closed (for affordance cache invalidation). */
|
|
58
|
+
onClosed?: (platformId: string, capabilityId: string) => void;
|
|
59
|
+
}
|
|
60
|
+
export declare function createCircuitBreakerManager(options: CircuitBreakerManagerOptions): CircuitBreakerManager;
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CircuitBreakerManager — T-BTS.C.5
|
|
3
|
+
*
|
|
4
|
+
* Core logic: State machine (Closed → Open → HalfOpen → Closed/Open).
|
|
5
|
+
* - Closed: counts consecutive failures; threshold hit → Open.
|
|
6
|
+
* - Open: rejects execution; cooldown elapsed → HalfOpen.
|
|
7
|
+
* - HalfOpen: initiates runWetProbe via ProbeSignalAdapter.
|
|
8
|
+
* - strict side-effect → probe_policy_denied, stays HalfOpen.
|
|
9
|
+
* - probe success → Closed + invalidate affordance cache (DR-003).
|
|
10
|
+
* - probe failure → Open.
|
|
11
|
+
*
|
|
12
|
+
* Persistence:
|
|
13
|
+
* - State stored in SQLite `circuit_breaker_state` table (v7-003).
|
|
14
|
+
* - Loads previous state on first access.
|
|
15
|
+
*
|
|
16
|
+
* Dependencies:
|
|
17
|
+
* - `StateDatabase` from `../../../../storage/db/index.js`
|
|
18
|
+
* - `WetProbeRunner` from `../../../../connectors/base/wet-probe-runner.js`
|
|
19
|
+
* - `CapabilityContractRegistryV7` from `../../../../connectors/base/manifest-v7.js`
|
|
20
|
+
* - `ProbeSignalAdapter` from `../probe-signal-adapter.js`
|
|
21
|
+
*
|
|
22
|
+
* Boundary:
|
|
23
|
+
* - Manager decides WHEN to probe (DR-002); connector-system executes it.
|
|
24
|
+
* - Does NOT execute HTTP directly — delegates to ProbeSignalAdapter.
|
|
25
|
+
*
|
|
26
|
+
* Test coverage: tests/unit/body/circuit-breaker-manager.test.ts
|
|
27
|
+
*/
|
|
28
|
+
export function createCircuitBreakerManager(options) {
|
|
29
|
+
const { database: { sqlite }, probeAdapter, failureThreshold = 3, cooldownMs = 30_000, onClosed, } = options;
|
|
30
|
+
function loadRecord(platformId, capabilityId) {
|
|
31
|
+
const result = sqlite.exec(`SELECT * FROM circuit_breaker_state
|
|
32
|
+
WHERE platform_id = ? AND capability_id = ?`, [platformId, capabilityId]);
|
|
33
|
+
if (result.length === 0 || result[0].values.length === 0) {
|
|
34
|
+
return {
|
|
35
|
+
platformId,
|
|
36
|
+
capabilityId,
|
|
37
|
+
state: "closed",
|
|
38
|
+
failureCount: 0,
|
|
39
|
+
consecutiveFailures: 0,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const cols = result[0].columns;
|
|
43
|
+
const get = (name) => result[0].values[0][cols.indexOf(name)];
|
|
44
|
+
return {
|
|
45
|
+
platformId,
|
|
46
|
+
capabilityId,
|
|
47
|
+
state: get("state"),
|
|
48
|
+
failureCount: get("failure_count") ?? 0,
|
|
49
|
+
consecutiveFailures: get("consecutive_failures") ?? 0,
|
|
50
|
+
lastFailureAt: get("last_failure_at") ?? undefined,
|
|
51
|
+
openedAt: get("opened_at") ?? undefined,
|
|
52
|
+
lastProbeAt: get("last_probe_at") ?? undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function saveRecord(rec) {
|
|
56
|
+
const now = new Date().toISOString();
|
|
57
|
+
sqlite.run(`INSERT INTO circuit_breaker_state
|
|
58
|
+
(platform_id, capability_id, state, failure_count, consecutive_failures,
|
|
59
|
+
last_failure_at, opened_at, last_probe_at, updated_at)
|
|
60
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
61
|
+
ON CONFLICT(platform_id, capability_id) DO UPDATE SET
|
|
62
|
+
state = excluded.state,
|
|
63
|
+
failure_count = excluded.failure_count,
|
|
64
|
+
consecutive_failures = excluded.consecutive_failures,
|
|
65
|
+
last_failure_at = excluded.last_failure_at,
|
|
66
|
+
opened_at = excluded.opened_at,
|
|
67
|
+
last_probe_at = excluded.last_probe_at,
|
|
68
|
+
updated_at = excluded.updated_at`, [
|
|
69
|
+
rec.platformId,
|
|
70
|
+
rec.capabilityId,
|
|
71
|
+
rec.state,
|
|
72
|
+
rec.failureCount,
|
|
73
|
+
rec.consecutiveFailures,
|
|
74
|
+
rec.lastFailureAt ?? null,
|
|
75
|
+
rec.openedAt ?? null,
|
|
76
|
+
rec.lastProbeAt ?? null,
|
|
77
|
+
now,
|
|
78
|
+
]);
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
async evaluateFailure(platformId, capabilityId) {
|
|
82
|
+
const rec = loadRecord(platformId, capabilityId);
|
|
83
|
+
rec.consecutiveFailures += 1;
|
|
84
|
+
rec.failureCount += 1;
|
|
85
|
+
rec.lastFailureAt = new Date().toISOString();
|
|
86
|
+
if (rec.state === "closed" && rec.consecutiveFailures >= failureThreshold) {
|
|
87
|
+
rec.state = "open";
|
|
88
|
+
rec.openedAt = rec.lastFailureAt;
|
|
89
|
+
}
|
|
90
|
+
else if (rec.state === "half_open") {
|
|
91
|
+
// HalfOpen + failure → back to Open
|
|
92
|
+
rec.state = "open";
|
|
93
|
+
rec.openedAt = rec.lastFailureAt;
|
|
94
|
+
}
|
|
95
|
+
// open + failure stays open
|
|
96
|
+
saveRecord(rec);
|
|
97
|
+
return rec.state;
|
|
98
|
+
},
|
|
99
|
+
async evaluateSuccess(platformId, capabilityId) {
|
|
100
|
+
const rec = loadRecord(platformId, capabilityId);
|
|
101
|
+
if (rec.state === "half_open") {
|
|
102
|
+
rec.state = "closed";
|
|
103
|
+
rec.consecutiveFailures = 0;
|
|
104
|
+
rec.openedAt = undefined;
|
|
105
|
+
if (onClosed) {
|
|
106
|
+
onClosed(platformId, capabilityId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (rec.state === "closed") {
|
|
110
|
+
rec.consecutiveFailures = 0;
|
|
111
|
+
}
|
|
112
|
+
// open + success stays open (should not happen via normal path)
|
|
113
|
+
saveRecord(rec);
|
|
114
|
+
return rec.state;
|
|
115
|
+
},
|
|
116
|
+
async canExecute(platformId, capabilityId) {
|
|
117
|
+
const rec = loadRecord(platformId, capabilityId);
|
|
118
|
+
if (rec.state === "closed")
|
|
119
|
+
return true;
|
|
120
|
+
if (rec.state === "half_open")
|
|
121
|
+
return true; // allow limited probe traffic
|
|
122
|
+
if (rec.state === "open") {
|
|
123
|
+
if (rec.openedAt) {
|
|
124
|
+
const elapsed = Date.now() - new Date(rec.openedAt).getTime();
|
|
125
|
+
if (elapsed >= cooldownMs) {
|
|
126
|
+
return true; // let caller attempt, will transition to HalfOpen
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
return true;
|
|
132
|
+
},
|
|
133
|
+
async getState(platformId, capabilityId) {
|
|
134
|
+
return loadRecord(platformId, capabilityId).state;
|
|
135
|
+
},
|
|
136
|
+
async attemptReset(platformId, capabilityId) {
|
|
137
|
+
const rec = loadRecord(platformId, capabilityId);
|
|
138
|
+
if (rec.state !== "half_open" && rec.state !== "open") {
|
|
139
|
+
return rec.state;
|
|
140
|
+
}
|
|
141
|
+
// If cooldown has elapsed from Open, transition to HalfOpen and probe
|
|
142
|
+
if (rec.state === "open" && rec.openedAt) {
|
|
143
|
+
const elapsed = Date.now() - new Date(rec.openedAt).getTime();
|
|
144
|
+
if (elapsed < cooldownMs) {
|
|
145
|
+
return rec.state; // still cooling
|
|
146
|
+
}
|
|
147
|
+
rec.state = "half_open";
|
|
148
|
+
saveRecord(rec);
|
|
149
|
+
}
|
|
150
|
+
// HalfOpen: run wet probe
|
|
151
|
+
const probeResult = await probeAdapter.runAndRecordProbe(platformId, capabilityId, options.registry);
|
|
152
|
+
rec.lastProbeAt = new Date().toISOString();
|
|
153
|
+
if (probeResult.httpStatus === 0 && probeResult.actualStatus === "unavailable") {
|
|
154
|
+
// probe_policy_denied or network failure → stay HalfOpen
|
|
155
|
+
saveRecord(rec);
|
|
156
|
+
return rec.state;
|
|
157
|
+
}
|
|
158
|
+
if (probeResult.actualStatus === "available") {
|
|
159
|
+
rec.state = "closed";
|
|
160
|
+
rec.consecutiveFailures = 0;
|
|
161
|
+
rec.openedAt = undefined;
|
|
162
|
+
if (onClosed) {
|
|
163
|
+
onClosed(platformId, capabilityId);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
rec.state = "open";
|
|
168
|
+
rec.openedAt = new Date().toISOString();
|
|
169
|
+
}
|
|
170
|
+
saveRecord(rec);
|
|
171
|
+
return rec.state;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProbeSignalAdapter — T-BTS.C.4
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Bridge between WetProbeRunner and state-memory.
|
|
5
|
+
* 1. Runs a wet probe for a capability.
|
|
6
|
+
* 2. Persists the CapabilityProbeResult to state-memory.
|
|
7
|
+
* 3. If the probe indicates degradation/unavailability, records a
|
|
8
|
+
* corresponding ToolExperience row with triggerSource="probe".
|
|
9
|
+
*
|
|
10
|
+
* Dependencies:
|
|
11
|
+
* - `WetProbeRunner` from `../../../connectors/base/wet-probe-runner.js`
|
|
12
|
+
* - `CapabilityContractRegistryV7` from `../../../connectors/base/manifest-v7.js`
|
|
13
|
+
* - `CapabilityProbeResultStore` from `../../../storage/services/tool-experience-store.js`
|
|
14
|
+
* - `ToolExperienceStore` from `../../../storage/services/tool-experience-store.js`
|
|
15
|
+
* - `ExperienceWriter` from `./tool-experience/experience-writer.js`
|
|
16
|
+
*
|
|
17
|
+
* Boundary:
|
|
18
|
+
* - Does NOT modify breaker state — caller (CircuitBreakerManager) decides.
|
|
19
|
+
* - probePolicyDenied is treated as a valid result, not an error.
|
|
20
|
+
*
|
|
21
|
+
* Test coverage: tests/unit/body/probe-signal-adapter.test.ts
|
|
22
|
+
*/
|
|
23
|
+
import type { WetProbeRunner } from "../../../connectors/base/wet-probe-runner.js";
|
|
24
|
+
import type { CapabilityContractRegistryV7 } from "../../../connectors/base/manifest-v7.js";
|
|
25
|
+
import type { CapabilityProbeResultStore, ToolExperienceStore } from "../../../storage/services/tool-experience-store.js";
|
|
26
|
+
export interface ProbeSignalAdapter {
|
|
27
|
+
runAndRecordProbe(platformId: string, capabilityId: string, registry: CapabilityContractRegistryV7): Promise<{
|
|
28
|
+
actualStatus: string;
|
|
29
|
+
httpStatus: number;
|
|
30
|
+
recorded: boolean;
|
|
31
|
+
experienceRecorded: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createProbeSignalAdapter(deps: {
|
|
35
|
+
wetProbeRunner: WetProbeRunner;
|
|
36
|
+
probeResultStore: CapabilityProbeResultStore;
|
|
37
|
+
toolExperienceStore: ToolExperienceStore;
|
|
38
|
+
}): ProbeSignalAdapter;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProbeSignalAdapter — T-BTS.C.4
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Bridge between WetProbeRunner and state-memory.
|
|
5
|
+
* 1. Runs a wet probe for a capability.
|
|
6
|
+
* 2. Persists the CapabilityProbeResult to state-memory.
|
|
7
|
+
* 3. If the probe indicates degradation/unavailability, records a
|
|
8
|
+
* corresponding ToolExperience row with triggerSource="probe".
|
|
9
|
+
*
|
|
10
|
+
* Dependencies:
|
|
11
|
+
* - `WetProbeRunner` from `../../../connectors/base/wet-probe-runner.js`
|
|
12
|
+
* - `CapabilityContractRegistryV7` from `../../../connectors/base/manifest-v7.js`
|
|
13
|
+
* - `CapabilityProbeResultStore` from `../../../storage/services/tool-experience-store.js`
|
|
14
|
+
* - `ToolExperienceStore` from `../../../storage/services/tool-experience-store.js`
|
|
15
|
+
* - `ExperienceWriter` from `./tool-experience/experience-writer.js`
|
|
16
|
+
*
|
|
17
|
+
* Boundary:
|
|
18
|
+
* - Does NOT modify breaker state — caller (CircuitBreakerManager) decides.
|
|
19
|
+
* - probePolicyDenied is treated as a valid result, not an error.
|
|
20
|
+
*
|
|
21
|
+
* Test coverage: tests/unit/body/probe-signal-adapter.test.ts
|
|
22
|
+
*/
|
|
23
|
+
import { createExperienceWriter } from "./tool-experience/experience-writer.js";
|
|
24
|
+
export function createProbeSignalAdapter(deps) {
|
|
25
|
+
const { wetProbeRunner, probeResultStore, toolExperienceStore } = deps;
|
|
26
|
+
const experienceWriter = createExperienceWriter(toolExperienceStore);
|
|
27
|
+
return {
|
|
28
|
+
async runAndRecordProbe(platformId, capabilityId, registry) {
|
|
29
|
+
const result = await wetProbeRunner.runWetProbe(platformId, capabilityId, registry);
|
|
30
|
+
// Persist probe result
|
|
31
|
+
await probeResultStore.appendProbeResult(result.probeResult);
|
|
32
|
+
// Record experience for non-success probes
|
|
33
|
+
let experienceRecorded = false;
|
|
34
|
+
if (result.probeResult.actualStatus !== "available") {
|
|
35
|
+
const mockResult = {
|
|
36
|
+
status: "terminal_failure",
|
|
37
|
+
failureClass: "transport_failure",
|
|
38
|
+
metadata: {
|
|
39
|
+
platformId,
|
|
40
|
+
channel: "api_rest",
|
|
41
|
+
latencyMs: 0,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
await experienceWriter.recordExperience({
|
|
45
|
+
connectorId: platformId,
|
|
46
|
+
capabilityId,
|
|
47
|
+
result: mockResult,
|
|
48
|
+
triggerSource: "probe",
|
|
49
|
+
});
|
|
50
|
+
experienceRecorded = true;
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
actualStatus: result.probeResult.actualStatus,
|
|
54
|
+
httpStatus: result.httpStatus,
|
|
55
|
+
recorded: true,
|
|
56
|
+
experienceRecorded,
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AffordanceAssembler — T-BTS.C.1
|
|
3
|
+
*
|
|
4
|
+
* Core logic: Assemble a platform→capability affordance map from the v7
|
|
5
|
+
* capability registry and recent probe results.
|
|
6
|
+
*
|
|
7
|
+
* Mapping rules (probe actualStatus → affordance status):
|
|
8
|
+
* - available → safe
|
|
9
|
+
* - degraded → exploratory
|
|
10
|
+
* - unavailable → unavailable
|
|
11
|
+
* - no probe + credential required → needs_auth
|
|
12
|
+
* - no probe + no credential required → unavailable
|
|
13
|
+
*
|
|
14
|
+
* Caching:
|
|
15
|
+
* - TTL cache (default 30s) keyed by serialized scope.
|
|
16
|
+
* - Invalidate on explicit call or when underlying data changes.
|
|
17
|
+
*
|
|
18
|
+
* Performance target: P95 < 1s for 50 manifests.
|
|
19
|
+
*
|
|
20
|
+
* Dependencies:
|
|
21
|
+
* - `CapabilityContractRegistryV7` from `../../../../connectors/base/manifest-v7.js`
|
|
22
|
+
* - `AffordanceItem`, `AffordanceMap`, `AffordanceContextScope`
|
|
23
|
+
* from `../../../../shared/types/v7-entities.js`
|
|
24
|
+
* - `applyAffordanceContextScope` from `./affordance-context-scope.js`
|
|
25
|
+
*
|
|
26
|
+
* Boundary:
|
|
27
|
+
* - Does NOT perform HTTP probes — reads cached probe results only.
|
|
28
|
+
* - Credential-bearing entries are excluded (ADR-003).
|
|
29
|
+
* - Returns a plain object; caller decides persistence.
|
|
30
|
+
*
|
|
31
|
+
* Test coverage: tests/unit/body/affordance-assembler.test.ts
|
|
32
|
+
*/
|
|
33
|
+
import type { CapabilityContractRegistryV7 } from "../../../../connectors/base/manifest-v7.js";
|
|
34
|
+
import type { AffordanceMap, AffordanceContextScope, ProbeActualStatus } from "../../../../shared/types/v7-entities.js";
|
|
35
|
+
export interface ProbeResultReader {
|
|
36
|
+
getLatestProbeResult(platformId: string, capabilityId: string): {
|
|
37
|
+
actualStatus: ProbeActualStatus;
|
|
38
|
+
createdAt: string;
|
|
39
|
+
} | undefined;
|
|
40
|
+
}
|
|
41
|
+
export interface AffordanceAssembler {
|
|
42
|
+
assembleAffordanceMap(scope?: AffordanceContextScope): Promise<AffordanceMap>;
|
|
43
|
+
invalidateCache(): void;
|
|
44
|
+
}
|
|
45
|
+
export interface AffordanceAssemblerOptions {
|
|
46
|
+
registry: CapabilityContractRegistryV7;
|
|
47
|
+
probeReader: ProbeResultReader;
|
|
48
|
+
credentialRequired?: (platformId: string, capabilityId: string) => boolean;
|
|
49
|
+
ttlMs?: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function createAffordanceAssembler(options: AffordanceAssemblerOptions): AffordanceAssembler;
|