@haaaiawd/second-nature 0.1.33 → 0.1.38
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/agent-inner-guide.md +25 -0
- package/index.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/commands/goal.d.ts +1 -0
- package/runtime/cli/commands/goal.js +1 -0
- package/runtime/cli/index.js +3 -3
- package/runtime/cli/ops/heartbeat-surface.d.ts +6 -0
- package/runtime/cli/ops/heartbeat-surface.js +2 -0
- package/runtime/cli/ops/ops-router.js +221 -92
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +24 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +42 -1
- package/runtime/connectors/base/contract.d.ts +10 -0
- package/runtime/connectors/base/map-life-evidence.js +5 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +7 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +25 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +5 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +10 -1
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +5 -0
- package/runtime/core/second-nature/orchestrator/guard-layer.js +24 -1
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.d.ts +20 -0
- package/runtime/core/second-nature/quiet/run-source-backed-quiet.js +32 -2
- package/runtime/guidance/capability-class.d.ts +38 -0
- package/runtime/guidance/capability-class.js +65 -0
- package/runtime/guidance/guidance-assembler.d.ts +2 -0
- package/runtime/guidance/guidance-assembler.js +16 -4
- package/runtime/guidance/guidance-draft-service.js +5 -5
- package/runtime/guidance/impulse-assembler.d.ts +71 -0
- package/runtime/guidance/impulse-assembler.js +103 -0
- package/runtime/guidance/index.d.ts +2 -0
- package/runtime/guidance/index.js +2 -0
- package/runtime/guidance/outreach-strategy-selector.d.ts +13 -0
- package/runtime/guidance/outreach-strategy-selector.js +2 -2
- package/runtime/guidance/template-registry.d.ts +15 -2
- package/runtime/guidance/template-registry.js +38 -1
- package/runtime/guidance/types.d.ts +13 -1
- package/runtime/storage/goal/agent-goal-store.d.ts +2 -0
- package/runtime/storage/goal/agent-goal-store.js +28 -1
- package/runtime/storage/services/tool-experience-store.js +9 -1
|
@@ -23,6 +23,8 @@ import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
|
|
|
23
23
|
import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
|
|
24
24
|
import type { NarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
|
|
25
25
|
import type { NarrativeTracePayload } from "../../../observability/services/lived-experience-audit.js";
|
|
26
|
+
import type { ExperienceWriter } from "../body/tool-experience/experience-writer.js";
|
|
27
|
+
import type { QuietDreamSchedulePort } from "../quiet/run-source-backed-quiet.js";
|
|
26
28
|
export interface HeartbeatDecisionTracePayload {
|
|
27
29
|
scope: RuntimeScope;
|
|
28
30
|
status: HeartbeatCycleStatus;
|
|
@@ -43,12 +45,14 @@ export interface HeartbeatOutreachDispatchDeps {
|
|
|
43
45
|
/** Optional Quiet orchestration: when set, quiet/reflection allows run source-backed Quiet writer (T2.3.3). */
|
|
44
46
|
export interface HeartbeatQuietWorkflowDeps {
|
|
45
47
|
workspaceRoot: string;
|
|
48
|
+
/** v7 T-V7C.C.3: when present, a successful Quiet write auto-triggers Dream scheduling. */
|
|
49
|
+
dreamSchedulePort?: QuietDreamSchedulePort;
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
49
53
|
* Exported for unit tests (CR-M1 wiring).
|
|
50
54
|
*/
|
|
51
|
-
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot">): Promise<HeartbeatCycleResult>;
|
|
55
|
+
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot" | "experienceWriter">): Promise<HeartbeatCycleResult>;
|
|
52
56
|
export interface HeartbeatDeps {
|
|
53
57
|
/** Load snapshot inputs from state-system */
|
|
54
58
|
loadSnapshotInputs: () => Promise<SnapshotInputs>;
|
|
@@ -71,6 +75,8 @@ export interface HeartbeatDeps {
|
|
|
71
75
|
workspaceRoot?: string;
|
|
72
76
|
/** T2.4.1: when present, planner resolves platform-specific intents. */
|
|
73
77
|
connectorRegistry?: CapabilityContractRegistry;
|
|
78
|
+
/** v7 T-V7C.C.2: when present, connector attempts write ToolExperience with triggerSource="heartbeat". */
|
|
79
|
+
experienceWriter?: ExperienceWriter;
|
|
74
80
|
}
|
|
75
81
|
/**
|
|
76
82
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -38,6 +38,8 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
38
38
|
day,
|
|
39
39
|
userInterestSnapshot: inputs.userInterestSnapshot,
|
|
40
40
|
workspaceRoot: deps.quietWorkflow.workspaceRoot,
|
|
41
|
+
// v7 T-V7C.C.3: pass Dream schedule port so Quiet completion triggers Dream.
|
|
42
|
+
dreamSchedulePort: deps.quietWorkflow.dreamSchedulePort,
|
|
41
43
|
});
|
|
42
44
|
return quietRun.result;
|
|
43
45
|
}
|
|
@@ -64,6 +66,8 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
const decisionId = `decision:${intent.id}:${Date.now()}`;
|
|
69
|
+
// T-V7C.C.4: inject identity from EmbodiedContext into connector request (readable, no credential)
|
|
70
|
+
const platformHandle = runtime.identity?.platformHandles.find((h) => h.platformId === intent.platformId)?.handle;
|
|
67
71
|
const result = await deps.connectorExecutor.executeEffect({
|
|
68
72
|
platformId: intent.platformId,
|
|
69
73
|
intent: toCapabilityIntent(intent),
|
|
@@ -71,6 +75,12 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
71
75
|
decisionId,
|
|
72
76
|
intentId: intent.id,
|
|
73
77
|
idempotencyKey: `idem:${intent.id}:${Date.now()}`,
|
|
78
|
+
identity: platformHandle || runtime.identity?.canonicalName
|
|
79
|
+
? {
|
|
80
|
+
platformHandle,
|
|
81
|
+
canonicalName: runtime.identity?.canonicalName,
|
|
82
|
+
}
|
|
83
|
+
: undefined,
|
|
74
84
|
});
|
|
75
85
|
// T3.3.1: on success, map connector result to life evidence and append.
|
|
76
86
|
// On failure or empty result, no evidence is fabricated — attempt audit
|
|
@@ -96,6 +106,21 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
96
106
|
console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
97
107
|
}
|
|
98
108
|
}
|
|
109
|
+
// v7 T-V7C.C.2: record ToolExperience for all connector attempts in heartbeat.
|
|
110
|
+
if (deps.experienceWriter) {
|
|
111
|
+
try {
|
|
112
|
+
await deps.experienceWriter.recordExperience({
|
|
113
|
+
connectorId: intent.platformId,
|
|
114
|
+
capabilityId: toCapabilityIntent(intent),
|
|
115
|
+
result,
|
|
116
|
+
triggerSource: "heartbeat",
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
121
|
+
console.warn(`[heartbeat] ToolExperience record failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
99
124
|
const base = {
|
|
100
125
|
scope: "rhythm",
|
|
101
126
|
status: "intent_selected",
|
|
@@ -5,6 +5,7 @@ import type { ContinuitySnapshot, ControlPlaneSourceRef } from "../types.js";
|
|
|
5
5
|
import type { RhythmPolicy } from "../rhythm/rhythm-policy.js";
|
|
6
6
|
import { type PlannerRhythmWindowSlice } from "../rhythm/planner-rhythm-window.js";
|
|
7
7
|
import type { SnapshotInputs } from "./snapshot-builder.js";
|
|
8
|
+
import type { AffordanceMap } from "../../../shared/types/v7-entities.js";
|
|
8
9
|
export interface PlannerLifeEvidenceSlice {
|
|
9
10
|
evidenceRefs: ControlPlaneSourceRef[];
|
|
10
11
|
platformEventCount: number;
|
|
@@ -23,6 +24,10 @@ export interface HeartbeatRuntimeSnapshot {
|
|
|
23
24
|
hardGuards: HardGuardDeps;
|
|
24
25
|
narrativeState?: import("../../../storage/narrative/narrative-state-store.js").NarrativeState;
|
|
25
26
|
relationshipMemory?: import("../../../storage/relationship/relationship-memory-store.js").RelationshipMemory;
|
|
27
|
+
/** v7: affordance map for breaker-aware guard evaluation (T-V7C.C.2). */
|
|
28
|
+
affordanceMap?: AffordanceMap;
|
|
29
|
+
/** T-V7C.C.4: identity profile for connector request identity injection. */
|
|
30
|
+
identity?: import("../../../shared/types/v7-entities.js").IdentityProfile;
|
|
26
31
|
}
|
|
27
32
|
export declare function buildLifeEvidenceSliceFromInputs(inputs: SnapshotInputs): PlannerLifeEvidenceSlice;
|
|
28
33
|
export declare function buildHardGuardDeps(continuity: ContinuitySnapshot, inputs: SnapshotInputs): HardGuardDeps;
|
|
@@ -31,5 +31,14 @@ export function buildHeartbeatRuntimeSnapshot(timestamp, inputs, continuity) {
|
|
|
31
31
|
const rhythmWindow = buildPlannerRhythmWindow(timestamp, continuity, policy);
|
|
32
32
|
const lifeEvidence = buildLifeEvidenceSliceFromInputs(inputs);
|
|
33
33
|
const hardGuards = buildHardGuardDeps(continuity, inputs);
|
|
34
|
-
return {
|
|
34
|
+
return {
|
|
35
|
+
continuity,
|
|
36
|
+
lifeEvidence,
|
|
37
|
+
rhythmWindow,
|
|
38
|
+
hardGuards,
|
|
39
|
+
narrativeState: inputs.narrativeState,
|
|
40
|
+
relationshipMemory: inputs.relationshipMemory,
|
|
41
|
+
affordanceMap: inputs.affordanceMap,
|
|
42
|
+
identity: inputs.identity,
|
|
43
|
+
};
|
|
35
44
|
}
|
|
@@ -12,6 +12,7 @@ import type { DeliveryCapabilitySnapshot } from "../outreach/delivery-target.js"
|
|
|
12
12
|
import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
|
|
13
13
|
import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
|
|
14
14
|
import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
|
|
15
|
+
import type { AffordanceMap, IdentityProfile } from "../../../shared/types/v7-entities.js";
|
|
15
16
|
export interface SnapshotInputs {
|
|
16
17
|
mode: TopLevelMode;
|
|
17
18
|
currentWindowId: string;
|
|
@@ -58,6 +59,10 @@ export interface SnapshotInputs {
|
|
|
58
59
|
narrativeState?: NarrativeState;
|
|
59
60
|
/** When present, planner uses relationship memory to influence outreach timing. */
|
|
60
61
|
relationshipMemory?: RelationshipMemory;
|
|
62
|
+
/** v7: affordance map for breaker-aware guard evaluation (T-V7C.C.2). */
|
|
63
|
+
affordanceMap?: AffordanceMap;
|
|
64
|
+
/** T-V7C.C.4: identity profile for connector request identity injection. */
|
|
65
|
+
identity?: IdentityProfile;
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* Build a ContinuitySnapshot from loaded inputs.
|
|
@@ -42,6 +42,27 @@ export function evaluateHardGuards(intent, runtime) {
|
|
|
42
42
|
if (!isSourceBacked(intent)) {
|
|
43
43
|
reasons.push("missing_source_refs");
|
|
44
44
|
}
|
|
45
|
+
// v7: Affordance / breaker guard (T-V7C.C.2)
|
|
46
|
+
if ((intent.effectClass === "connector_action" ||
|
|
47
|
+
intent.effectClass === "external_platform_action") &&
|
|
48
|
+
runtime.affordanceMap &&
|
|
49
|
+
intent.platformId) {
|
|
50
|
+
const platformItems = runtime.affordanceMap[intent.platformId] ?? [];
|
|
51
|
+
const match = intent.capabilityIntent
|
|
52
|
+
? platformItems.find((i) => i.capabilityId === intent.capabilityIntent)
|
|
53
|
+
: platformItems.find((i) => i.intent === intent.summary);
|
|
54
|
+
if (match) {
|
|
55
|
+
if (match.status === "painful") {
|
|
56
|
+
reasons.push("connector_circuit_open");
|
|
57
|
+
}
|
|
58
|
+
else if (match.status === "unavailable") {
|
|
59
|
+
reasons.push("affordance_unavailable");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
reasons.push("affordance_unavailable");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
45
66
|
const key = intentFingerprint(intent);
|
|
46
67
|
if (runtime.hardGuards.hasDuplicateIntent(key)) {
|
|
47
68
|
reasons.push("duplicate_intent");
|
|
@@ -74,7 +95,9 @@ export function evaluateHardGuards(intent, runtime) {
|
|
|
74
95
|
}
|
|
75
96
|
const duplicate = reasons.includes("duplicate_intent");
|
|
76
97
|
const cooldown = reasons.includes("outreach_cooldown");
|
|
77
|
-
|
|
98
|
+
const circuitOpen = reasons.includes("connector_circuit_open");
|
|
99
|
+
const affordanceUnavailable = reasons.includes("affordance_unavailable");
|
|
100
|
+
if (duplicate || cooldown || circuitOpen || affordanceUnavailable) {
|
|
78
101
|
return {
|
|
79
102
|
verdict: "defer",
|
|
80
103
|
reasons,
|
|
@@ -1,17 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Quiet / reflection orchestration: empty evidence → empty_state; otherwise coverage-gated artifact (T2.3.3).
|
|
3
|
+
*
|
|
4
|
+
* v7 T-V7C.C.3: After a successful Quiet artifact write, if a DreamSchedulePort is provided,
|
|
5
|
+
* automatically trigger scheduleDream(quiet_completion). Skip reason is embedded in HeartbeatCycleResult
|
|
6
|
+
* reasons when the scheduler returns "skipped" (e.g. lock held).
|
|
3
7
|
*/
|
|
4
8
|
import type { CandidateIntent } from "../types.js";
|
|
5
9
|
import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
|
|
6
10
|
import type { HeartbeatCycleResult } from "../heartbeat/signal.js";
|
|
7
11
|
import { type QuietArtifactAck } from "../../../storage/quiet/quiet-artifact-writer.js";
|
|
8
12
|
import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
|
|
13
|
+
/**
|
|
14
|
+
* Minimal port for triggering Dream after Quiet completion (T-V7C.C.3).
|
|
15
|
+
* Kept narrow so run-source-backed-quiet does not take a hard dependency on dream-scheduler.
|
|
16
|
+
*/
|
|
17
|
+
export interface QuietDreamSchedulePort {
|
|
18
|
+
scheduleDream(params: {
|
|
19
|
+
triggerKind: "quiet_completion";
|
|
20
|
+
runId: string;
|
|
21
|
+
traceId: string;
|
|
22
|
+
}): Promise<{
|
|
23
|
+
status: "started" | "skipped" | "queued";
|
|
24
|
+
reason?: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
9
27
|
export interface RunSourceBackedQuietParams {
|
|
10
28
|
candidate: CandidateIntent;
|
|
11
29
|
runtime: HeartbeatRuntimeSnapshot;
|
|
12
30
|
day: string;
|
|
13
31
|
userInterestSnapshot?: UserInterestSnapshot;
|
|
14
32
|
workspaceRoot?: string;
|
|
33
|
+
/** v7 T-V7C.C.3: when present, a successful Quiet artifact write auto-triggers Dream scheduling. */
|
|
34
|
+
dreamSchedulePort?: QuietDreamSchedulePort;
|
|
15
35
|
}
|
|
16
36
|
export interface RunSourceBackedQuietResult {
|
|
17
37
|
result: HeartbeatCycleResult;
|
|
@@ -11,8 +11,33 @@ function toGuidanceRef(r) {
|
|
|
11
11
|
observedAt: r.observedAt,
|
|
12
12
|
};
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* v7 T-V7C.C.3: Fire-and-forget Dream schedule after successful Quiet write.
|
|
16
|
+
* Returns the schedule status reason string to embed in HeartbeatCycleResult reasons.
|
|
17
|
+
* Never throws — Dream scheduling failure must not break the Quiet cycle result.
|
|
18
|
+
*/
|
|
19
|
+
async function maybeScheduleDreamAfterQuiet(dreamSchedulePort, day) {
|
|
20
|
+
if (!dreamSchedulePort)
|
|
21
|
+
return undefined;
|
|
22
|
+
try {
|
|
23
|
+
const result = await dreamSchedulePort.scheduleDream({
|
|
24
|
+
triggerKind: "quiet_completion",
|
|
25
|
+
runId: `dream:quiet_completion:${day}:${Date.now()}`,
|
|
26
|
+
traceId: `trace:quiet_completion:${day}:${Date.now()}`,
|
|
27
|
+
});
|
|
28
|
+
if (result.status === "skipped") {
|
|
29
|
+
return `quiet_dream_skip:${result.reason ?? "lock_held"}`;
|
|
30
|
+
}
|
|
31
|
+
return "quiet_dream_scheduled";
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35
|
+
console.warn(`[run-source-backed-quiet] Dream schedule failed: ${msg}`);
|
|
36
|
+
return `quiet_dream_schedule_error:${msg.slice(0, 60)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
14
39
|
export async function runSourceBackedQuiet(params) {
|
|
15
|
-
const { candidate, runtime, day, userInterestSnapshot, workspaceRoot } = params;
|
|
40
|
+
const { candidate, runtime, day, userInterestSnapshot, workspaceRoot, dreamSchedulePort } = params;
|
|
16
41
|
const empty = isLifeEvidenceSliceEmpty(runtime.lifeEvidence);
|
|
17
42
|
if (empty) {
|
|
18
43
|
const input = {
|
|
@@ -117,12 +142,17 @@ export async function runSourceBackedQuiet(params) {
|
|
|
117
142
|
const p = await persistQuietArtifactToWorkspace(workspaceRoot, ack, reportWrite);
|
|
118
143
|
persistedRelativePath = p.relativePath;
|
|
119
144
|
}
|
|
145
|
+
// v7 T-V7C.C.3: After a successful source-backed Quiet write, auto-trigger Dream scheduling.
|
|
146
|
+
const dreamReason = await maybeScheduleDreamAfterQuiet(dreamSchedulePort, day);
|
|
147
|
+
const reasons = ["quiet_artifact_written", ...gq.hints.slice(0, 2)];
|
|
148
|
+
if (dreamReason)
|
|
149
|
+
reasons.push(dreamReason);
|
|
120
150
|
return {
|
|
121
151
|
result: {
|
|
122
152
|
scope: "rhythm",
|
|
123
153
|
status: "intent_selected",
|
|
124
154
|
selectedIntentId: candidate.id,
|
|
125
|
-
reasons
|
|
155
|
+
reasons,
|
|
126
156
|
},
|
|
127
157
|
artifactAck: ack,
|
|
128
158
|
persistedRelativePath,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capability-class.ts — T-V7C.C.4R
|
|
3
|
+
*
|
|
4
|
+
* Core logic: infer CapabilityClass from capabilityIntent string prefix.
|
|
5
|
+
*
|
|
6
|
+
* CapabilityClass is the middle axis between intentKind (why) and platform (where),
|
|
7
|
+
* used by ImpulseAssembler to select appropriate behavioral impulse templates.
|
|
8
|
+
*
|
|
9
|
+
* Classification rules (prefix-based, not EffectSemanticsClass — execution layer is intentionally
|
|
10
|
+
* kept separate from expression layer):
|
|
11
|
+
* feed.* → consume (browsing / reading feeds)
|
|
12
|
+
* notification.* → consume (reading notifications, not interacting)
|
|
13
|
+
* work.* → discover (research / task discovery)
|
|
14
|
+
* post.* → broadcast (publishing, primary expression)
|
|
15
|
+
* comment.* → interact (replying to others' content)
|
|
16
|
+
* message.* → interact (private messages / DMs)
|
|
17
|
+
* task.* → claim (claiming work items)
|
|
18
|
+
* agent.* → null (keepalive/internal — excluded from impulse system)
|
|
19
|
+
* unknown/custom → broadcast (safe default for unrecognized side-effect capabilities)
|
|
20
|
+
*
|
|
21
|
+
* Boundary:
|
|
22
|
+
* - Pure function, zero side effects.
|
|
23
|
+
* - Does NOT consult EffectSemanticsClass (execution-policy.ts) — those layers must not couple.
|
|
24
|
+
* - Custom capabilities declared by Claw without a prefix match default to "broadcast".
|
|
25
|
+
*
|
|
26
|
+
* Test coverage: tests/unit/guidance/capability-class.test.ts
|
|
27
|
+
*/
|
|
28
|
+
/** The expression-layer classification of a capability. */
|
|
29
|
+
export type CapabilityClass = "consume" | "broadcast" | "interact" | "discover" | "claim";
|
|
30
|
+
/**
|
|
31
|
+
* Infer CapabilityClass from a capabilityIntent string.
|
|
32
|
+
*
|
|
33
|
+
* Returns null for agent.* capabilities (keepalive / internal — no impulse injection).
|
|
34
|
+
* Returns "broadcast" for unrecognized custom capabilities (safe default).
|
|
35
|
+
*/
|
|
36
|
+
export declare function inferCapabilityClass(capabilityIntent: string): CapabilityClass | null;
|
|
37
|
+
/** Map from CapabilityClass to its default intentKind-style scene, for impulse lookup fallback. */
|
|
38
|
+
export declare const CAPABILITY_CLASS_SCENE_MAP: Readonly<Record<CapabilityClass, string>>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* capability-class.ts — T-V7C.C.4R
|
|
3
|
+
*
|
|
4
|
+
* Core logic: infer CapabilityClass from capabilityIntent string prefix.
|
|
5
|
+
*
|
|
6
|
+
* CapabilityClass is the middle axis between intentKind (why) and platform (where),
|
|
7
|
+
* used by ImpulseAssembler to select appropriate behavioral impulse templates.
|
|
8
|
+
*
|
|
9
|
+
* Classification rules (prefix-based, not EffectSemanticsClass — execution layer is intentionally
|
|
10
|
+
* kept separate from expression layer):
|
|
11
|
+
* feed.* → consume (browsing / reading feeds)
|
|
12
|
+
* notification.* → consume (reading notifications, not interacting)
|
|
13
|
+
* work.* → discover (research / task discovery)
|
|
14
|
+
* post.* → broadcast (publishing, primary expression)
|
|
15
|
+
* comment.* → interact (replying to others' content)
|
|
16
|
+
* message.* → interact (private messages / DMs)
|
|
17
|
+
* task.* → claim (claiming work items)
|
|
18
|
+
* agent.* → null (keepalive/internal — excluded from impulse system)
|
|
19
|
+
* unknown/custom → broadcast (safe default for unrecognized side-effect capabilities)
|
|
20
|
+
*
|
|
21
|
+
* Boundary:
|
|
22
|
+
* - Pure function, zero side effects.
|
|
23
|
+
* - Does NOT consult EffectSemanticsClass (execution-policy.ts) — those layers must not couple.
|
|
24
|
+
* - Custom capabilities declared by Claw without a prefix match default to "broadcast".
|
|
25
|
+
*
|
|
26
|
+
* Test coverage: tests/unit/guidance/capability-class.test.ts
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Infer CapabilityClass from a capabilityIntent string.
|
|
30
|
+
*
|
|
31
|
+
* Returns null for agent.* capabilities (keepalive / internal — no impulse injection).
|
|
32
|
+
* Returns "broadcast" for unrecognized custom capabilities (safe default).
|
|
33
|
+
*/
|
|
34
|
+
export function inferCapabilityClass(capabilityIntent) {
|
|
35
|
+
if (!capabilityIntent || typeof capabilityIntent !== "string")
|
|
36
|
+
return null;
|
|
37
|
+
const prefix = capabilityIntent.split(".")[0]?.toLowerCase() ?? "";
|
|
38
|
+
switch (prefix) {
|
|
39
|
+
case "agent":
|
|
40
|
+
return null; // Keepalive/internal: excluded from impulse system entirely
|
|
41
|
+
case "feed":
|
|
42
|
+
case "notification":
|
|
43
|
+
return "consume";
|
|
44
|
+
case "work":
|
|
45
|
+
return "discover";
|
|
46
|
+
case "post":
|
|
47
|
+
return "broadcast";
|
|
48
|
+
case "comment":
|
|
49
|
+
case "message":
|
|
50
|
+
return "interact";
|
|
51
|
+
case "task":
|
|
52
|
+
return "claim";
|
|
53
|
+
default:
|
|
54
|
+
// Custom or unrecognized capability — default to broadcast (outward expression)
|
|
55
|
+
return "broadcast";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** Map from CapabilityClass to its default intentKind-style scene, for impulse lookup fallback. */
|
|
59
|
+
export const CAPABILITY_CLASS_SCENE_MAP = {
|
|
60
|
+
consume: "explore",
|
|
61
|
+
discover: "explore",
|
|
62
|
+
broadcast: "social",
|
|
63
|
+
interact: "reply",
|
|
64
|
+
claim: "work",
|
|
65
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { type PlatformImpulsePort } from "./impulse-assembler.js";
|
|
1
2
|
import type { GuidancePayload, GuidanceUnavailable, PersonaCandidate, SceneContext } from "./types.js";
|
|
2
3
|
export declare function assembleGuidance(input: {
|
|
3
4
|
sceneContext: SceneContext | null | undefined;
|
|
4
5
|
personaCandidates?: PersonaCandidate[];
|
|
6
|
+
platformImpulsePort?: PlatformImpulsePort;
|
|
5
7
|
}): Promise<GuidancePayload | GuidanceUnavailable>;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { buildMinimalGuidanceFallback } from "./fallback.js";
|
|
2
2
|
import { buildOutputGuard } from "./output-guard.js";
|
|
3
3
|
import { selectPersonaSnippets } from "./persona-selection.js";
|
|
4
|
-
import { getBaselineAtmosphereTemplate
|
|
4
|
+
import { getBaselineAtmosphereTemplate } from "./template-registry.js";
|
|
5
|
+
import { assembleImpulse } from "./impulse-assembler.js";
|
|
5
6
|
async function buildAtmosphere(sceneContext) {
|
|
6
7
|
const template = getBaselineAtmosphereTemplate();
|
|
7
8
|
return {
|
|
@@ -12,11 +13,21 @@ async function buildAtmosphere(sceneContext) {
|
|
|
12
13
|
reviewStatus: template.reviewStatus,
|
|
13
14
|
};
|
|
14
15
|
}
|
|
15
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Select impulses using the dual-axis capabilityClass assembler (T-V7C.C.4R).
|
|
18
|
+
*
|
|
19
|
+
* Fallback chain: platform-specific → capabilityClass preset → intentKind → []
|
|
20
|
+
*/
|
|
21
|
+
async function selectImpulses(sceneContext, deps) {
|
|
16
22
|
if (sceneContext.sceneType === "explain" || sceneContext.sceneType === "user_reply") {
|
|
17
23
|
return [];
|
|
18
24
|
}
|
|
19
|
-
|
|
25
|
+
const result = await assembleImpulse({
|
|
26
|
+
sceneType: sceneContext.sceneType,
|
|
27
|
+
capabilityIntent: sceneContext.capabilityIntent,
|
|
28
|
+
platformId: sceneContext.platformId,
|
|
29
|
+
}, deps);
|
|
30
|
+
return result.impulse ? [result.impulse] : [];
|
|
20
31
|
}
|
|
21
32
|
export async function assembleGuidance(input) {
|
|
22
33
|
if (!input.sceneContext) {
|
|
@@ -26,10 +37,11 @@ export async function assembleGuidance(input) {
|
|
|
26
37
|
};
|
|
27
38
|
}
|
|
28
39
|
const sceneContext = input.sceneContext;
|
|
40
|
+
const deps = { platformImpulsePort: input.platformImpulsePort };
|
|
29
41
|
try {
|
|
30
42
|
const [atmosphere, impulses] = await Promise.all([
|
|
31
43
|
buildAtmosphere(sceneContext),
|
|
32
|
-
selectImpulses(sceneContext),
|
|
44
|
+
selectImpulses(sceneContext, deps),
|
|
33
45
|
]);
|
|
34
46
|
const personaDecision = selectPersonaSnippets({
|
|
35
47
|
sceneContext,
|
|
@@ -19,16 +19,16 @@ const SCENE_KINDS = [
|
|
|
19
19
|
"reconnect",
|
|
20
20
|
];
|
|
21
21
|
function buildDraftText(request, claims) {
|
|
22
|
-
const anchor = claims.map((c) => c.text).join("
|
|
22
|
+
const anchor = claims.map((c) => c.text).join(";");
|
|
23
23
|
switch (request.sceneKind) {
|
|
24
24
|
case "outreach":
|
|
25
|
-
return
|
|
25
|
+
return `有件事想跟你分享,正好碰到了:${anchor}`;
|
|
26
26
|
case "follow_up":
|
|
27
|
-
return
|
|
27
|
+
return `接着上次聊的说一下:${anchor}`;
|
|
28
28
|
case "reconnect":
|
|
29
|
-
return
|
|
29
|
+
return `好久不见,最近有个东西让我想到你:${anchor}`;
|
|
30
30
|
default:
|
|
31
|
-
return
|
|
31
|
+
return `关于 ${request.sceneKind}:${anchor}`;
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
export async function generateGuidanceDraft(request, deps) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* impulse-assembler.ts — T-V7C.C.4R
|
|
3
|
+
*
|
|
4
|
+
* Core logic: three-level fallback impulse selection.
|
|
5
|
+
*
|
|
6
|
+
* Priority chain (highest → lowest):
|
|
7
|
+
* 1. platform-specific impulse — Claw-defined per platformId, loaded from workspace
|
|
8
|
+
* 2. capabilityClass preset — derived from capabilityIntent prefix via inferCapabilityClass
|
|
9
|
+
* 3. intentKind fallback — existing scene type impulse (social/outreach/reply/quiet)
|
|
10
|
+
* 4. null — no impulse (baseline atmosphere still applies)
|
|
11
|
+
*
|
|
12
|
+
* Exclusions:
|
|
13
|
+
* - agent.* capabilities → null always (keepalive/internal, not an expression action)
|
|
14
|
+
* - explore/work capabilityClass impulses → approved and active (T-V7C.C.4R review complete)
|
|
15
|
+
*
|
|
16
|
+
* Boundary:
|
|
17
|
+
* - Pure function composition; no I/O except the optional platformImpulsePort.
|
|
18
|
+
* - Does NOT write state or emit events.
|
|
19
|
+
* - SceneContext is enriched with capabilityIntent + platformId as optional fields
|
|
20
|
+
* to carry the dual-axis context without breaking existing SceneContext consumers.
|
|
21
|
+
*
|
|
22
|
+
* Test coverage: tests/unit/guidance/impulse-assembler.test.ts
|
|
23
|
+
*/
|
|
24
|
+
import type { ImpulseBlock, GuidanceSceneType } from "./types.js";
|
|
25
|
+
import { type CapabilityClass } from "./capability-class.js";
|
|
26
|
+
/** Extended scene context carrying dual-axis impulse selection inputs. */
|
|
27
|
+
export interface ImpulseSelectionContext {
|
|
28
|
+
/** The intent kind (why) — maps to intentKind fallback impulse. */
|
|
29
|
+
sceneType: GuidanceSceneType;
|
|
30
|
+
/** The capability being executed (what physical form). e.g. "post.publish", "feed.read" */
|
|
31
|
+
capabilityIntent?: string;
|
|
32
|
+
/** The platform being targeted. Used for platform-specific impulse lookup. */
|
|
33
|
+
platformId?: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Port for loading platform-specific impulse overrides.
|
|
37
|
+
* Claw implements this by writing impulse files to the workspace.
|
|
38
|
+
* When absent, the assembler skips platform-specific lookup gracefully.
|
|
39
|
+
*/
|
|
40
|
+
export interface PlatformImpulsePort {
|
|
41
|
+
/**
|
|
42
|
+
* Load a platform-specific impulse for the given platformId + capabilityClass.
|
|
43
|
+
* Returns null when no override is defined.
|
|
44
|
+
*/
|
|
45
|
+
loadPlatformImpulse(input: {
|
|
46
|
+
platformId: string;
|
|
47
|
+
capabilityClass: CapabilityClass;
|
|
48
|
+
}): Promise<ImpulseBlock | null>;
|
|
49
|
+
}
|
|
50
|
+
export interface ImpulseAssemblerResult {
|
|
51
|
+
/** The selected impulse, or null if no applicable impulse exists. */
|
|
52
|
+
impulse: ImpulseBlock | null;
|
|
53
|
+
/** Which level of the fallback chain was used. */
|
|
54
|
+
source: "platform_specific" | "capability_class" | "intent_kind" | "none";
|
|
55
|
+
/** The inferred capability class (null for agent.* or unknown intent). */
|
|
56
|
+
capabilityClass: CapabilityClass | null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Select the most specific impulse for a given scene + capability context.
|
|
60
|
+
*
|
|
61
|
+
* Fallback chain:
|
|
62
|
+
* platform-specific → capabilityClass preset → intentKind → null
|
|
63
|
+
*/
|
|
64
|
+
export declare function assembleImpulse(ctx: ImpulseSelectionContext, deps: {
|
|
65
|
+
platformImpulsePort?: PlatformImpulsePort;
|
|
66
|
+
}): Promise<ImpulseAssemblerResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Synchronous variant for contexts where capabilityClass + intentKind are sufficient
|
|
69
|
+
* and no platform-specific port is needed (e.g. guidance_payload ops command preview).
|
|
70
|
+
*/
|
|
71
|
+
export declare function assembleImpulseSync(ctx: ImpulseSelectionContext): ImpulseAssemblerResult;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* impulse-assembler.ts — T-V7C.C.4R
|
|
3
|
+
*
|
|
4
|
+
* Core logic: three-level fallback impulse selection.
|
|
5
|
+
*
|
|
6
|
+
* Priority chain (highest → lowest):
|
|
7
|
+
* 1. platform-specific impulse — Claw-defined per platformId, loaded from workspace
|
|
8
|
+
* 2. capabilityClass preset — derived from capabilityIntent prefix via inferCapabilityClass
|
|
9
|
+
* 3. intentKind fallback — existing scene type impulse (social/outreach/reply/quiet)
|
|
10
|
+
* 4. null — no impulse (baseline atmosphere still applies)
|
|
11
|
+
*
|
|
12
|
+
* Exclusions:
|
|
13
|
+
* - agent.* capabilities → null always (keepalive/internal, not an expression action)
|
|
14
|
+
* - explore/work capabilityClass impulses → approved and active (T-V7C.C.4R review complete)
|
|
15
|
+
*
|
|
16
|
+
* Boundary:
|
|
17
|
+
* - Pure function composition; no I/O except the optional platformImpulsePort.
|
|
18
|
+
* - Does NOT write state or emit events.
|
|
19
|
+
* - SceneContext is enriched with capabilityIntent + platformId as optional fields
|
|
20
|
+
* to carry the dual-axis context without breaking existing SceneContext consumers.
|
|
21
|
+
*
|
|
22
|
+
* Test coverage: tests/unit/guidance/impulse-assembler.test.ts
|
|
23
|
+
*/
|
|
24
|
+
import { inferCapabilityClass, CAPABILITY_CLASS_SCENE_MAP, } from "./capability-class.js";
|
|
25
|
+
import { getImpulseTemplate, getCapabilityClassImpulseTemplate, } from "./template-registry.js";
|
|
26
|
+
// ─── Core assembly logic ──────────────────────────────────────────────────────
|
|
27
|
+
/**
|
|
28
|
+
* Select the most specific impulse for a given scene + capability context.
|
|
29
|
+
*
|
|
30
|
+
* Fallback chain:
|
|
31
|
+
* platform-specific → capabilityClass preset → intentKind → null
|
|
32
|
+
*/
|
|
33
|
+
export async function assembleImpulse(ctx, deps) {
|
|
34
|
+
// Infer capability class from capabilityIntent prefix
|
|
35
|
+
const capabilityClass = ctx.capabilityIntent
|
|
36
|
+
? inferCapabilityClass(ctx.capabilityIntent)
|
|
37
|
+
: null;
|
|
38
|
+
// agent.* → excluded entirely
|
|
39
|
+
if (ctx.capabilityIntent && inferCapabilityClass(ctx.capabilityIntent) === null &&
|
|
40
|
+
ctx.capabilityIntent.startsWith("agent.")) {
|
|
41
|
+
return { impulse: null, source: "none", capabilityClass: null };
|
|
42
|
+
}
|
|
43
|
+
// ── Level 1: platform-specific ──────────────────────────────────────────────
|
|
44
|
+
if (ctx.platformId && capabilityClass && deps.platformImpulsePort) {
|
|
45
|
+
try {
|
|
46
|
+
const platformImpulse = await deps.platformImpulsePort.loadPlatformImpulse({
|
|
47
|
+
platformId: ctx.platformId,
|
|
48
|
+
capabilityClass,
|
|
49
|
+
});
|
|
50
|
+
if (platformImpulse) {
|
|
51
|
+
return { impulse: platformImpulse, source: "platform_specific", capabilityClass };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// Port failure → fall through gracefully
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ── Level 2: capabilityClass preset ─────────────────────────────────────────
|
|
59
|
+
if (capabilityClass) {
|
|
60
|
+
const ccImpulseKind = CAPABILITY_CLASS_SCENE_MAP[capabilityClass];
|
|
61
|
+
const ccImpulse = getCapabilityClassImpulseTemplate(ccImpulseKind);
|
|
62
|
+
if (ccImpulse) {
|
|
63
|
+
return { impulse: ccImpulse, source: "capability_class", capabilityClass };
|
|
64
|
+
}
|
|
65
|
+
// explore/work are pending review → fall through to intentKind
|
|
66
|
+
}
|
|
67
|
+
// ── Level 3: intentKind fallback ─────────────────────────────────────────────
|
|
68
|
+
const sceneType = ctx.sceneType;
|
|
69
|
+
if (sceneType !== "explain" && sceneType !== "user_reply") {
|
|
70
|
+
const intentImpulse = getImpulseTemplate(sceneType);
|
|
71
|
+
return { impulse: intentImpulse, source: "intent_kind", capabilityClass };
|
|
72
|
+
}
|
|
73
|
+
// ── Level 4: no impulse ───────────────────────────────────────────────────────
|
|
74
|
+
return { impulse: null, source: "none", capabilityClass };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Synchronous variant for contexts where capabilityClass + intentKind are sufficient
|
|
78
|
+
* and no platform-specific port is needed (e.g. guidance_payload ops command preview).
|
|
79
|
+
*/
|
|
80
|
+
export function assembleImpulseSync(ctx) {
|
|
81
|
+
const capabilityClass = ctx.capabilityIntent
|
|
82
|
+
? inferCapabilityClass(ctx.capabilityIntent)
|
|
83
|
+
: null;
|
|
84
|
+
// agent.* excluded
|
|
85
|
+
if (ctx.capabilityIntent?.startsWith("agent.")) {
|
|
86
|
+
return { impulse: null, source: "none", capabilityClass: null };
|
|
87
|
+
}
|
|
88
|
+
// capabilityClass preset (sync — no platform port available)
|
|
89
|
+
if (capabilityClass) {
|
|
90
|
+
const ccImpulseKind = CAPABILITY_CLASS_SCENE_MAP[capabilityClass];
|
|
91
|
+
const ccImpulse = getCapabilityClassImpulseTemplate(ccImpulseKind);
|
|
92
|
+
if (ccImpulse) {
|
|
93
|
+
return { impulse: ccImpulse, source: "capability_class", capabilityClass };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// intentKind fallback
|
|
97
|
+
const sceneType = ctx.sceneType;
|
|
98
|
+
if (sceneType !== "explain" && sceneType !== "user_reply") {
|
|
99
|
+
const intentImpulse = getImpulseTemplate(sceneType);
|
|
100
|
+
return { impulse: intentImpulse, source: "intent_kind", capabilityClass };
|
|
101
|
+
}
|
|
102
|
+
return { impulse: null, source: "none", capabilityClass };
|
|
103
|
+
}
|
|
@@ -5,6 +5,8 @@ export * from "./output-guard.js";
|
|
|
5
5
|
export * from "./fallback.js";
|
|
6
6
|
export * from "./template-registry.js";
|
|
7
7
|
export * from "./review-workflow.js";
|
|
8
|
+
export * from "./capability-class.js";
|
|
9
|
+
export * from "./impulse-assembler.js";
|
|
8
10
|
export * from "./guidance-assembler.js";
|
|
9
11
|
export * from "./outreach-draft-schema.js";
|
|
10
12
|
export * from "./draft-outreach-message.js";
|
|
@@ -5,6 +5,8 @@ export * from "./output-guard.js";
|
|
|
5
5
|
export * from "./fallback.js";
|
|
6
6
|
export * from "./template-registry.js";
|
|
7
7
|
export * from "./review-workflow.js";
|
|
8
|
+
export * from "./capability-class.js";
|
|
9
|
+
export * from "./impulse-assembler.js";
|
|
8
10
|
export * from "./guidance-assembler.js";
|
|
9
11
|
export * from "./outreach-draft-schema.js";
|
|
10
12
|
export * from "./draft-outreach-message.js";
|