@haaaiawd/second-nature 0.1.24 → 0.1.26
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/index.js +78 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -5
- package/runtime/cli/commands/goal.d.ts +28 -0
- package/runtime/cli/commands/goal.js +163 -0
- package/runtime/cli/commands/index.js +38 -3
- package/runtime/cli/explain/resolve-subject.js +3 -0
- package/runtime/cli/ops/ops-router.d.ts +1 -1
- package/runtime/cli/ops/ops-router.js +63 -1
- package/runtime/cli/ops/workspace-heartbeat-runner.d.ts +6 -0
- package/runtime/cli/ops/workspace-heartbeat-runner.js +35 -1
- package/runtime/cli/read-models/index.d.ts +14 -2
- package/runtime/cli/read-models/index.js +403 -101
- package/runtime/cli/read-models/types.d.ts +90 -3
- package/runtime/core/second-nature/feedback/owner-reply-feedback.d.ts +46 -0
- package/runtime/core/second-nature/feedback/owner-reply-feedback.js +159 -0
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +11 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +78 -10
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.d.ts +2 -0
- package/runtime/core/second-nature/heartbeat/runtime-snapshot.js +1 -1
- package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +16 -2
- package/runtime/core/second-nature/index.d.ts +1 -0
- package/runtime/core/second-nature/index.js +1 -0
- package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +16 -3
- package/runtime/core/second-nature/orchestrator/goal-priority.js +10 -9
- package/runtime/core/second-nature/orchestrator/intent-planner.d.ts +29 -1
- package/runtime/core/second-nature/orchestrator/intent-planner.js +154 -79
- package/runtime/core/second-nature/orchestrator/narrative-update.js +23 -9
- package/runtime/core/second-nature/orchestrator/platform-capability-router.d.ts +34 -0
- package/runtime/core/second-nature/orchestrator/platform-capability-router.js +115 -0
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.d.ts +3 -1
- package/runtime/core/second-nature/outreach/build-outreach-draft-request.js +39 -1
- package/runtime/core/second-nature/outreach/dispatch-user-outreach.js +21 -2
- package/runtime/guidance/draft-outreach-message.js +14 -1
- package/runtime/guidance/outreach-draft-schema.d.ts +104 -0
- package/runtime/guidance/outreach-draft-schema.js +14 -0
- package/runtime/observability/audit/audit-envelope.d.ts +1 -1
- package/runtime/observability/query/explain-query.d.ts +3 -0
- package/runtime/observability/query/explain-query.js +9 -0
- package/runtime/observability/services/lived-experience-audit.d.ts +22 -0
- package/runtime/observability/services/lived-experience-audit.js +30 -0
- package/runtime/shared/types/credential.d.ts +1 -1
- package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
- package/runtime/storage/db/schema/narrative-state.d.ts +1 -1
- package/runtime/storage/db/schema/narrative-state.js +2 -2
- package/runtime/storage/services/credential-vault.d.ts +18 -0
- package/runtime/storage/services/credential-vault.js +73 -3
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { isLifeEvidenceSliceEmpty } from "../heartbeat/runtime-snapshot.js";
|
|
2
2
|
import { buildHeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
|
|
3
|
+
import { resolvePlatformForIntent, } from "./platform-capability-router.js";
|
|
4
|
+
import { isGoalRelatedToCandidate } from "./goal-priority.js";
|
|
3
5
|
const MAX_CANDIDATE_INTENTS = 6;
|
|
4
6
|
const OBLIGATION_SOURCE = [
|
|
5
7
|
{ id: "obligation-anchor", kind: "workspace_artifact", uri: "workspace://obligations/pending" },
|
|
@@ -8,72 +10,131 @@ function evidenceRefsForConnector(runtime) {
|
|
|
8
10
|
if (!isLifeEvidenceSliceEmpty(runtime.lifeEvidence) && runtime.lifeEvidence.evidenceRefs.length > 0) {
|
|
9
11
|
return runtime.lifeEvidence.evidenceRefs.slice(0, 8);
|
|
10
12
|
}
|
|
11
|
-
if (!isLifeEvidenceSliceEmpty(runtime.lifeEvidence)) {
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
id: "life-evidence-summary",
|
|
15
|
-
kind: "connector_result",
|
|
16
|
-
uri: `workspace://life-evidence/counts/${runtime.lifeEvidence.platformEventCount}/${runtime.lifeEvidence.workEventCount}`,
|
|
17
|
-
},
|
|
18
|
-
];
|
|
19
|
-
}
|
|
20
13
|
return [];
|
|
21
14
|
}
|
|
22
15
|
function isAllowedKind(kind, runtime) {
|
|
23
16
|
return runtime.rhythmWindow.allowedIntentKinds.includes(kind);
|
|
24
17
|
}
|
|
25
|
-
function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
18
|
+
function focusMatchesKind(focus, kind) {
|
|
19
|
+
const lower = focus.toLowerCase();
|
|
20
|
+
switch (kind) {
|
|
21
|
+
case "work":
|
|
22
|
+
return lower.includes("work") || lower.includes("obligation") || lower.includes("task");
|
|
23
|
+
case "exploration":
|
|
24
|
+
return lower.includes("explor") || lower.includes("opportunit") || lower.includes("scan") || lower.includes("discover");
|
|
25
|
+
case "social":
|
|
26
|
+
return lower.includes("social") || lower.includes("engage") || lower.includes("community");
|
|
27
|
+
case "outreach":
|
|
28
|
+
return lower.includes("outreach") || lower.includes("user") || lower.includes("proactive") || lower.includes("contact");
|
|
29
|
+
case "quiet":
|
|
30
|
+
return lower.includes("quiet") || lower.includes("bookkeep") || lower.includes("pause");
|
|
31
|
+
case "reflection":
|
|
32
|
+
return lower.includes("reflect") || lower.includes("narrative") || lower.includes("review");
|
|
33
|
+
case "maintenance":
|
|
34
|
+
return lower.includes("maintenance") || lower.includes("check") || lower.includes("upkeep");
|
|
35
|
+
default:
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const INTENT_CONFIGS = {
|
|
40
|
+
work: {
|
|
41
|
+
basePriority: 100,
|
|
42
|
+
effectClass: "connector_action",
|
|
43
|
+
summary: (platformId, detail) => platformId ? `fulfill obligation on ${platformId}: ${detail}` : `fulfill obligation: ${detail}`,
|
|
32
44
|
source: "obligation",
|
|
33
|
-
|
|
45
|
+
idPrefix: "intent-obligation",
|
|
46
|
+
idempotencyPrefix: "obligation",
|
|
47
|
+
},
|
|
48
|
+
exploration: {
|
|
49
|
+
basePriority: 70,
|
|
34
50
|
effectClass: "connector_action",
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
summary: (platformId) => platformId ? `scan platform opportunities on ${platformId}` : "scan platform opportunities",
|
|
52
|
+
source: "tick",
|
|
53
|
+
idPrefix: "intent-exploration",
|
|
54
|
+
idempotencyPrefix: "exploration",
|
|
55
|
+
},
|
|
56
|
+
social: {
|
|
57
|
+
basePriority: 60,
|
|
58
|
+
effectClass: "connector_action",
|
|
59
|
+
summary: (platformId) => platformId ? `engage social platforms on ${platformId}` : "engage social platforms",
|
|
60
|
+
source: "tick",
|
|
61
|
+
idPrefix: "intent-social",
|
|
62
|
+
idempotencyPrefix: "social",
|
|
63
|
+
},
|
|
64
|
+
outreach: {
|
|
65
|
+
basePriority: 40,
|
|
66
|
+
effectClass: "user_outreach",
|
|
67
|
+
summary: (platformId) => platformId ? `consider proactive user outreach on ${platformId}` : "consider proactive user outreach",
|
|
68
|
+
source: "tick",
|
|
69
|
+
idPrefix: "intent-outreach",
|
|
70
|
+
idempotencyPrefix: "outreach",
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
/**
|
|
74
|
+
* Factory for planning a candidate intent of a given kind.
|
|
75
|
+
* M-04: consolidates the previously separate plan{Work,Exploration,Social,Outreach}Intents.
|
|
76
|
+
*/
|
|
77
|
+
export function planIntentWithKind(kind, basePriority, runtime, context, registry, options) {
|
|
78
|
+
if (!isAllowedKind(kind, runtime))
|
|
42
79
|
return [];
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
80
|
+
const config = INTENT_CONFIGS[kind];
|
|
81
|
+
const platformId = resolvePlatformForIntent(kind, context ?? {}, registry);
|
|
82
|
+
let priority = basePriority;
|
|
83
|
+
// Social budget exhaustion → cap priority.
|
|
84
|
+
if (kind === "social" &&
|
|
85
|
+
runtime.continuity.budgets &&
|
|
86
|
+
runtime.continuity.budgets.socialUsed >= runtime.continuity.budgets.socialLimit) {
|
|
87
|
+
priority = 10;
|
|
88
|
+
}
|
|
89
|
+
// Narrative focus bias (preserved from original per-kind functions).
|
|
90
|
+
if (options?.narrativeState?.focus && focusMatchesKind(options.narrativeState.focus, kind)) {
|
|
91
|
+
priority += 15;
|
|
92
|
+
}
|
|
93
|
+
// Outreach suppression checks.
|
|
94
|
+
if (kind === "outreach") {
|
|
95
|
+
if (runtime.continuity.recentOutreachHashes.length > 3) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
if (options?.relationshipMemory && options.relationshipMemory.noReplyCount > 3) {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Work special case: multi-source from pending obligations.
|
|
103
|
+
if (kind === "work" && options?.multiSource) {
|
|
104
|
+
return options.multiSource.map((source, index) => ({
|
|
105
|
+
id: platformId ? `${config.idPrefix}-${platformId}-${index}` : `${config.idPrefix}-${index}`,
|
|
106
|
+
kind: "work",
|
|
107
|
+
priority: basePriority - index,
|
|
108
|
+
source: "obligation",
|
|
109
|
+
platformId,
|
|
110
|
+
summary: config.summary(platformId, source),
|
|
111
|
+
effectClass: config.effectClass,
|
|
112
|
+
sourceRefs: [...OBLIGATION_SOURCE],
|
|
113
|
+
idempotencyKey: platformId
|
|
114
|
+
? `${config.idempotencyPrefix}:${platformId}:${source}:${index}`
|
|
115
|
+
: `${config.idempotencyPrefix}:${source}:${index}`,
|
|
54
116
|
goalInfluenceRefs: [],
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
function planSocialIntents(runtime) {
|
|
59
|
-
if (!isAllowedKind("social", runtime))
|
|
60
|
-
return [];
|
|
61
|
-
const refs = evidenceRefsForConnector(runtime);
|
|
117
|
+
}));
|
|
118
|
+
}
|
|
119
|
+
const refs = kind === "work" ? [...OBLIGATION_SOURCE] : evidenceRefsForConnector(runtime);
|
|
62
120
|
return [
|
|
63
121
|
{
|
|
64
|
-
id:
|
|
65
|
-
kind
|
|
66
|
-
priority
|
|
67
|
-
source:
|
|
68
|
-
|
|
69
|
-
|
|
122
|
+
id: platformId ? `${config.idPrefix}-${platformId}` : config.idPrefix,
|
|
123
|
+
kind,
|
|
124
|
+
priority,
|
|
125
|
+
source: config.source,
|
|
126
|
+
platformId,
|
|
127
|
+
summary: config.summary(platformId),
|
|
128
|
+
effectClass: config.effectClass,
|
|
70
129
|
sourceRefs: refs,
|
|
71
|
-
idempotencyKey:
|
|
130
|
+
idempotencyKey: platformId
|
|
131
|
+
? `${config.idempotencyPrefix}:${platformId}`
|
|
132
|
+
: `${config.idempotencyPrefix}:${config.summary(undefined)}`,
|
|
72
133
|
goalInfluenceRefs: [],
|
|
73
134
|
},
|
|
74
135
|
];
|
|
75
136
|
}
|
|
76
|
-
function planQuietReflectionIntents(runtime) {
|
|
137
|
+
function planQuietReflectionIntents(runtime, _context, _registry) {
|
|
77
138
|
if (!runtime.rhythmWindow.quietBias && runtime.continuity.mode !== "quiet") {
|
|
78
139
|
return [];
|
|
79
140
|
}
|
|
@@ -120,31 +181,17 @@ function planQuietReflectionIntents(runtime) {
|
|
|
120
181
|
}
|
|
121
182
|
return out;
|
|
122
183
|
}
|
|
123
|
-
function planOutreachIntents(runtime) {
|
|
124
|
-
if (!isAllowedKind("outreach", runtime))
|
|
125
|
-
return [];
|
|
126
|
-
if (runtime.continuity.recentOutreachHashes.length > 3) {
|
|
127
|
-
return [];
|
|
128
|
-
}
|
|
129
|
-
const refs = evidenceRefsForConnector(runtime);
|
|
130
|
-
return [
|
|
131
|
-
{
|
|
132
|
-
id: "intent-outreach",
|
|
133
|
-
kind: "outreach",
|
|
134
|
-
priority: 40,
|
|
135
|
-
source: "tick",
|
|
136
|
-
summary: "consider proactive user outreach",
|
|
137
|
-
effectClass: "user_outreach",
|
|
138
|
-
sourceRefs: refs,
|
|
139
|
-
idempotencyKey: "outreach:consider proactive user outreach",
|
|
140
|
-
goalInfluenceRefs: [],
|
|
141
|
-
},
|
|
142
|
-
];
|
|
143
|
-
}
|
|
144
184
|
/**
|
|
145
185
|
* Plan ordered candidates for one heartbeat turn using rhythm window + life evidence slice.
|
|
146
186
|
*/
|
|
147
|
-
export function planCandidateIntents(runtime) {
|
|
187
|
+
export function planCandidateIntents(runtime, options) {
|
|
188
|
+
const context = {
|
|
189
|
+
acceptedGoals: options?.acceptedGoals,
|
|
190
|
+
evidenceRefs: runtime.lifeEvidence.evidenceRefs,
|
|
191
|
+
};
|
|
192
|
+
const registry = options?.connectorRegistry;
|
|
193
|
+
const narrativeState = options?.narrativeState ?? runtime.narrativeState;
|
|
194
|
+
const relationshipMemory = options?.relationshipMemory ?? runtime.relationshipMemory;
|
|
148
195
|
if (runtime.continuity.mode === "paused_for_interrupt") {
|
|
149
196
|
const pausedMaintenance = [
|
|
150
197
|
{
|
|
@@ -164,16 +211,44 @@ export function planCandidateIntents(runtime) {
|
|
|
164
211
|
.slice(0, MAX_CANDIDATE_INTENTS);
|
|
165
212
|
}
|
|
166
213
|
if (runtime.continuity.mode === "maintenance_only") {
|
|
167
|
-
return
|
|
214
|
+
return planIntentWithKind("work", INTENT_CONFIGS.work.basePriority, runtime, context, registry, { multiSource: runtime.continuity.pendingObligations })
|
|
215
|
+
.sort((a, b) => b.priority - a.priority)
|
|
216
|
+
.slice(0, MAX_CANDIDATE_INTENTS);
|
|
168
217
|
}
|
|
169
218
|
const intents = [
|
|
170
|
-
...
|
|
171
|
-
...
|
|
172
|
-
...
|
|
173
|
-
|
|
174
|
-
|
|
219
|
+
...planIntentWithKind("work", INTENT_CONFIGS.work.basePriority, runtime, context, registry, { multiSource: runtime.continuity.pendingObligations }),
|
|
220
|
+
...planIntentWithKind("exploration", INTENT_CONFIGS.exploration.basePriority, runtime, context, registry),
|
|
221
|
+
...planIntentWithKind("social", INTENT_CONFIGS.social.basePriority, runtime, context, registry, {
|
|
222
|
+
narrativeState,
|
|
223
|
+
budgetCheck: true,
|
|
224
|
+
}),
|
|
225
|
+
...planQuietReflectionIntents(runtime, context, registry),
|
|
226
|
+
...planIntentWithKind("outreach", INTENT_CONFIGS.outreach.basePriority, runtime, context, registry, {
|
|
227
|
+
narrativeState,
|
|
228
|
+
relationshipMemory,
|
|
229
|
+
}),
|
|
175
230
|
];
|
|
176
|
-
|
|
231
|
+
// Pre-fill goalInfluenceRefs for non-obligation intents before returning.
|
|
232
|
+
// applyGoalPriority will later refine/override with the same logic.
|
|
233
|
+
const acceptedGoals = options?.acceptedGoals?.filter((g) => g.status === "accepted" &&
|
|
234
|
+
(g.origin !== "agent_proposed" || g.acceptedBy === "policy_allowlist")) ?? [];
|
|
235
|
+
for (const intent of intents) {
|
|
236
|
+
if (intent.source === "obligation")
|
|
237
|
+
continue;
|
|
238
|
+
const related = acceptedGoals.filter((g) => isGoalRelatedToCandidate(g, intent));
|
|
239
|
+
if (related.length > 0) {
|
|
240
|
+
intent.goalInfluenceRefs = related.map((g) => g.goalId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// CR-02: apply narrative-focus bias globally across all candidate kinds.
|
|
244
|
+
const adjusted = intents.map((intent) => {
|
|
245
|
+
let priority = intent.priority;
|
|
246
|
+
if (narrativeState?.focus && focusMatchesKind(narrativeState.focus, intent.kind)) {
|
|
247
|
+
priority += 15;
|
|
248
|
+
}
|
|
249
|
+
return { ...intent, priority };
|
|
250
|
+
});
|
|
251
|
+
return adjusted
|
|
177
252
|
.filter((intent) => runtime.rhythmWindow.allowedIntentKinds.includes(intent.kind))
|
|
178
253
|
.sort((a, b) => b.priority - a.priority)
|
|
179
254
|
.slice(0, MAX_CANDIDATE_INTENTS);
|
|
@@ -11,19 +11,32 @@ function mapControlPlaneRefToSourceRef(ref) {
|
|
|
11
11
|
/**
|
|
12
12
|
* Compute narrative confidence based on source evidence.
|
|
13
13
|
*
|
|
14
|
-
* Formula:
|
|
14
|
+
* Formula: smooth sigmoid-like growth:
|
|
15
|
+
* - 0 sources → 0
|
|
16
|
+
* - 1 source → 0.35 (not 0.43; single-source is weak but non-zero)
|
|
17
|
+
* - 2 sources → 0.60 (beginning to be trustworthy)
|
|
18
|
+
* - 3 sources → 0.80 (strong confidence, not 1.0)
|
|
19
|
+
* - 4+ sources → 0.90 (cap below 1.0 to avoid false certainty)
|
|
20
|
+
* - Boost: +0.05 if corroborating life evidence exists
|
|
21
|
+
* - Hard cap at 0.95 (never claim 100% certainty from evidence count alone)
|
|
15
22
|
*
|
|
16
|
-
* Rationale:
|
|
17
|
-
*
|
|
18
|
-
* - Boost: +0.1 if any life evidence exists (signals corroboration)
|
|
19
|
-
* - Capped at 1.0 (100%)
|
|
23
|
+
* Rationale: linear 1/3 per source produces unnatural jumps.
|
|
24
|
+
* Logarithmic growth better models diminishing returns per extra source.
|
|
20
25
|
*/
|
|
21
26
|
function computeConfidence(intentSources, lifeEvidenceSources) {
|
|
22
27
|
if (intentSources === 0 && lifeEvidenceSources === 0)
|
|
23
28
|
return 0;
|
|
24
|
-
const base =
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
const base = intentSources === 0
|
|
30
|
+
? 0
|
|
31
|
+
: intentSources === 1
|
|
32
|
+
? 0.35
|
|
33
|
+
: intentSources === 2
|
|
34
|
+
? 0.60
|
|
35
|
+
: intentSources === 3
|
|
36
|
+
? 0.80
|
|
37
|
+
: 0.90;
|
|
38
|
+
const boost = lifeEvidenceSources > 0 ? 0.05 : 0;
|
|
39
|
+
return Math.min(base + boost, 0.95);
|
|
27
40
|
}
|
|
28
41
|
/**
|
|
29
42
|
* Build the next NarrativeState revision from a completed heartbeat cycle.
|
|
@@ -48,7 +61,8 @@ export function updateNarrativeAfterEffect(input) {
|
|
|
48
61
|
const sourceRefs = selectedIntent.sourceRefs.map(mapControlPlaneRefToSourceRef);
|
|
49
62
|
if (hasIntentSources || hasLifeEvidence) {
|
|
50
63
|
// Source-backed revision
|
|
51
|
-
|
|
64
|
+
// L-03: Use effectClass + id as dedup key instead of full summary text.
|
|
65
|
+
const progressEntry = `${selectedIntent.effectClass}: ${selectedIntent.id}`;
|
|
52
66
|
const progress = [...(prior?.progress ?? [])];
|
|
53
67
|
if (!progress.includes(progressEntry)) {
|
|
54
68
|
progress.push(progressEntry);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T2.4.1 — Platform-specific intent resolution.
|
|
3
|
+
*
|
|
4
|
+
* When accepted goals, narrative, or connector evidence point to a specific
|
|
5
|
+
* platform, the planner emits a `CandidateIntent` with an explicit
|
|
6
|
+
* `platformId`. If the platform cannot be inferred, the caller falls
|
|
7
|
+
* back to the generic connector_action path (platformId undefined).
|
|
8
|
+
*
|
|
9
|
+
* Boundaries:
|
|
10
|
+
* - Does NOT execute connectors; only resolves platform + capability.
|
|
11
|
+
* - Does NOT validate credentials; that is the guard layer's job.
|
|
12
|
+
* - Optional registry: when absent, resolution is best-effort from goals/evidence.
|
|
13
|
+
*/
|
|
14
|
+
import type { IntentKind } from "../types.js";
|
|
15
|
+
import type { ControlPlaneSourceRef } from "../types.js";
|
|
16
|
+
import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
|
|
17
|
+
/** Minimal goal shape accepted by the router to avoid coupling to AgentGoal. M-03 decoupling. */
|
|
18
|
+
interface GoalRouterContext {
|
|
19
|
+
goalId: string;
|
|
20
|
+
description: string;
|
|
21
|
+
completionCriteria?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface PlatformResolutionContext {
|
|
24
|
+
/** Accepted goals that may name a platform or capability. */
|
|
25
|
+
acceptedGoals?: GoalRouterContext[];
|
|
26
|
+
/** Evidence refs that may embed platform identity. */
|
|
27
|
+
evidenceRefs?: ControlPlaneSourceRef[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Resolve an explicit platformId for a candidate intent kind.
|
|
31
|
+
* Returns `undefined` when no unambiguous platform can be inferred.
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolvePlatformForIntent(kind: IntentKind, context: PlatformResolutionContext, registry?: CapabilityContractRegistry): string | undefined;
|
|
34
|
+
export {};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
function kindToCapability(kind) {
|
|
2
|
+
if (kind === "exploration")
|
|
3
|
+
return "feed.read";
|
|
4
|
+
if (kind === "social")
|
|
5
|
+
return "comment.reply";
|
|
6
|
+
if (kind === "work")
|
|
7
|
+
return "work.discover";
|
|
8
|
+
if (kind === "outreach")
|
|
9
|
+
return "message.send";
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
function getPlatformIds(registry) {
|
|
13
|
+
if (registry) {
|
|
14
|
+
return registry.listRegisteredPlatformIds();
|
|
15
|
+
}
|
|
16
|
+
// Fallback: built-in platforms when registry is absent (backward compat)
|
|
17
|
+
return ["moltbook", "instreet", "evomap"];
|
|
18
|
+
}
|
|
19
|
+
function extractPlatformIdsFromGoals(goals, kind, platformIds) {
|
|
20
|
+
const capability = kindToCapability(kind);
|
|
21
|
+
const results = new Set();
|
|
22
|
+
for (const goal of goals) {
|
|
23
|
+
const text = `${goal.description} ${goal.completionCriteria ?? ""}`.toLowerCase();
|
|
24
|
+
for (const pid of platformIds) {
|
|
25
|
+
if (text.includes(pid)) {
|
|
26
|
+
results.add(pid);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Also match if goal text contains the capability name (e.g. "feed.read")
|
|
30
|
+
if (capability && text.includes(capability.toLowerCase())) {
|
|
31
|
+
// capability alone doesn't tell us platform; keep for later
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...results];
|
|
35
|
+
}
|
|
36
|
+
function extractPlatformIdsFromEvidence(refs, platformIds) {
|
|
37
|
+
const results = new Set();
|
|
38
|
+
for (const ref of refs) {
|
|
39
|
+
if (ref.kind === "connector_result" && ref.id) {
|
|
40
|
+
for (const pid of platformIds) {
|
|
41
|
+
if (ref.id.includes(pid)) {
|
|
42
|
+
results.add(pid);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Parse platform:// URIs (e.g. platform://moltbook/feed.read)
|
|
47
|
+
if (ref.uri && ref.uri.startsWith("platform://")) {
|
|
48
|
+
const afterScheme = ref.uri.slice("platform://".length);
|
|
49
|
+
const platformPart = afterScheme.split("/")[0];
|
|
50
|
+
if (platformPart && platformIds.includes(platformPart)) {
|
|
51
|
+
results.add(platformPart);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// L-02: Also support namespace format moltbook:feed.read (connector-system §5.3)
|
|
55
|
+
if (ref.uri && !ref.uri.includes("://") && ref.uri.includes(":")) {
|
|
56
|
+
const nsPart = ref.uri.split(":")[0];
|
|
57
|
+
if (nsPart && platformIds.includes(nsPart)) {
|
|
58
|
+
results.add(nsPart);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return [...results];
|
|
63
|
+
}
|
|
64
|
+
function validatePlatformCapability(platformId, kind, registry) {
|
|
65
|
+
const capability = kindToCapability(kind);
|
|
66
|
+
if (!capability)
|
|
67
|
+
return false;
|
|
68
|
+
try {
|
|
69
|
+
return registry.hasCapability(platformId, capability);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
// H-08: Log registry validation failures for observability.
|
|
73
|
+
console.warn(`[platform-capability-router] Registry validation failed for ${platformId}:${capability}`, err);
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve an explicit platformId for a candidate intent kind.
|
|
79
|
+
* Returns `undefined` when no unambiguous platform can be inferred.
|
|
80
|
+
*/
|
|
81
|
+
export function resolvePlatformForIntent(kind, context, registry) {
|
|
82
|
+
const capability = kindToCapability(kind);
|
|
83
|
+
if (!capability) {
|
|
84
|
+
// Quiet, reflection, maintenance have no connector capability mapping.
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
const platformIds = getPlatformIds(registry);
|
|
88
|
+
const candidates = [];
|
|
89
|
+
if (context.acceptedGoals && context.acceptedGoals.length > 0) {
|
|
90
|
+
candidates.push(...extractPlatformIdsFromGoals(context.acceptedGoals, kind, platformIds));
|
|
91
|
+
}
|
|
92
|
+
if (context.evidenceRefs && context.evidenceRefs.length > 0) {
|
|
93
|
+
candidates.push(...extractPlatformIdsFromEvidence(context.evidenceRefs, platformIds));
|
|
94
|
+
}
|
|
95
|
+
// Deduplicate while preserving order
|
|
96
|
+
const ordered = [...new Set(candidates)];
|
|
97
|
+
if (ordered.length === 0) {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
if (ordered.length > 1) {
|
|
101
|
+
// Ambiguous: multiple platforms inferred → do not guess, return undefined.
|
|
102
|
+
// Guard layer will deny with "ambiguous_platform" reason.
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const single = ordered[0];
|
|
106
|
+
if (registry) {
|
|
107
|
+
if (validatePlatformCapability(single, kind, registry)) {
|
|
108
|
+
return single;
|
|
109
|
+
}
|
|
110
|
+
// Registry says unsupported → undefined (guard layer will deny)
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
// No registry: best-effort return the single candidate (backward compat)
|
|
114
|
+
return single;
|
|
115
|
+
}
|
|
@@ -3,4 +3,6 @@ import type { CandidateIntent } from "../types.js";
|
|
|
3
3
|
import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
|
|
4
4
|
import type { OutreachJudgment } from "./judge-outreach.js";
|
|
5
5
|
import type { DeliveryTargetResolution } from "./delivery-target.js";
|
|
6
|
-
|
|
6
|
+
import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
|
|
7
|
+
import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
|
|
8
|
+
export declare function buildOutreachDraftRequest(candidate: CandidateIntent, judgment: OutreachJudgment, snapshot: HeartbeatRuntimeSnapshot, delivery: DeliveryTargetResolution, narrativeState?: NarrativeState, relationshipMemory?: RelationshipMemory): OutreachDraftRequest;
|
|
@@ -40,7 +40,43 @@ function mapDeliveryVerdict(verdict) {
|
|
|
40
40
|
return "host_unsupported";
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
-
|
|
43
|
+
function buildNarrativeContext(state) {
|
|
44
|
+
if (!state)
|
|
45
|
+
return undefined;
|
|
46
|
+
return {
|
|
47
|
+
focus: state.focus || undefined,
|
|
48
|
+
progress: state.progress.length > 0 ? state.progress : undefined,
|
|
49
|
+
nextIntent: state.nextIntent || undefined,
|
|
50
|
+
sourceRefs: state.sourceRefs.map((r) => ({
|
|
51
|
+
id: r.sourceId,
|
|
52
|
+
kind: "user_anchor",
|
|
53
|
+
uri: r.url || "",
|
|
54
|
+
excerptHash: r.snippet,
|
|
55
|
+
observedAt: undefined,
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildRelationshipContext(memory) {
|
|
60
|
+
if (!memory)
|
|
61
|
+
return undefined;
|
|
62
|
+
const avgAffinity = memory.topicAffinities.length > 0
|
|
63
|
+
? memory.topicAffinities.reduce((s, t) => s + t.affinity, 0) /
|
|
64
|
+
memory.topicAffinities.length
|
|
65
|
+
: 0;
|
|
66
|
+
return {
|
|
67
|
+
tone: memory.tonePreference,
|
|
68
|
+
topicAffinities: memory.topicAffinities.map((t) => t.topic),
|
|
69
|
+
avgAffinity,
|
|
70
|
+
sourceRefs: memory.sourceRefs?.map((r) => ({
|
|
71
|
+
id: r.sourceId,
|
|
72
|
+
kind: "user_anchor",
|
|
73
|
+
uri: r.url || "",
|
|
74
|
+
excerptHash: r.snippet,
|
|
75
|
+
observedAt: undefined,
|
|
76
|
+
})),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export function buildOutreachDraftRequest(candidate, judgment, snapshot, delivery, narrativeState, relationshipMemory) {
|
|
44
80
|
const sceneType = delivery.verdict === "target_available" ? "outreach" : "fallback_candidate";
|
|
45
81
|
const riskLevel = delivery.verdict === "target_available" ? "medium" : "low";
|
|
46
82
|
return {
|
|
@@ -55,6 +91,8 @@ export function buildOutreachDraftRequest(candidate, judgment, snapshot, deliver
|
|
|
55
91
|
judgmentVerdict: judgment.verdict,
|
|
56
92
|
valueScore: judgment.valueScore,
|
|
57
93
|
interestRefs: toGuidanceRefs(judgment.interestRefs),
|
|
94
|
+
narrativeContext: buildNarrativeContext(narrativeState),
|
|
95
|
+
relationshipContext: buildRelationshipContext(relationshipMemory),
|
|
58
96
|
deliveryContext: {
|
|
59
97
|
deliveryVerdict: mapDeliveryVerdict(delivery.verdict),
|
|
60
98
|
wordingMode: delivery.verdict === "target_available" ? "sendable" : "not_sent_fallback_candidate",
|
|
@@ -3,6 +3,8 @@ import { writeOperatorFallback } from "../../../storage/fallback/write-operator-
|
|
|
3
3
|
import { judgeOutreach } from "./judge-outreach.js";
|
|
4
4
|
import { resolveDeliveryTarget } from "./delivery-target.js";
|
|
5
5
|
import { buildOutreachDraftRequest } from "./build-outreach-draft-request.js";
|
|
6
|
+
import { createNarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
|
|
7
|
+
import { createRelationshipMemoryStore } from "../../../storage/relationship/relationship-memory-store.js";
|
|
6
8
|
function toSourceRefs(refs) {
|
|
7
9
|
return refs.map((r) => ({ ...r }));
|
|
8
10
|
}
|
|
@@ -29,8 +31,25 @@ export async function dispatchUserOutreachIntent(input) {
|
|
|
29
31
|
};
|
|
30
32
|
}
|
|
31
33
|
const deliveryResolution = resolveDeliveryTarget(judgeInput.delivery);
|
|
34
|
+
// T2.3.1: load narrative/relationship context for source-backed draft
|
|
35
|
+
let narrativeState;
|
|
36
|
+
let relationshipMemory;
|
|
37
|
+
try {
|
|
38
|
+
const narrativeStore = createNarrativeStateStore(state);
|
|
39
|
+
narrativeState = (await narrativeStore.loadNarrativeState()) ?? undefined;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// degrade silently; draft proceeds without narrative context
|
|
43
|
+
}
|
|
44
|
+
try {
|
|
45
|
+
const relStore = createRelationshipMemoryStore(state);
|
|
46
|
+
relationshipMemory = (await relStore.loadRelationshipMemory()) ?? undefined;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// degrade silently; draft proceeds without relationship context
|
|
50
|
+
}
|
|
32
51
|
if (deliveryResolution.verdict !== "target_available") {
|
|
33
|
-
const req = buildOutreachDraftRequest(candidate, judgment, snapshot, deliveryResolution);
|
|
52
|
+
const req = buildOutreachDraftRequest(candidate, judgment, snapshot, deliveryResolution, narrativeState, relationshipMemory);
|
|
34
53
|
const draft = await guidance.draftOutreachMessage(req);
|
|
35
54
|
const fb = await writeOperatorFallback(state, {
|
|
36
55
|
reason: operatorReasonForUnavailable(deliveryResolution.verdict),
|
|
@@ -48,7 +67,7 @@ export async function dispatchUserOutreachIntent(input) {
|
|
|
48
67
|
fallbackRef: fb.fallbackRef,
|
|
49
68
|
};
|
|
50
69
|
}
|
|
51
|
-
const req = buildOutreachDraftRequest(candidate, judgment, snapshot, deliveryResolution);
|
|
70
|
+
const req = buildOutreachDraftRequest(candidate, judgment, snapshot, deliveryResolution, narrativeState, relationshipMemory);
|
|
52
71
|
const draft = await guidance.draftOutreachMessage(req);
|
|
53
72
|
if (draft.status !== "ready") {
|
|
54
73
|
return {
|
|
@@ -3,9 +3,22 @@
|
|
|
3
3
|
* Does not claim user-visible delivery when wordingMode is not_sent_fallback_candidate (T6.2.1 / ADR-004).
|
|
4
4
|
*/
|
|
5
5
|
import { safeParseOutreachDraftRequest } from "./outreach-draft-schema.js";
|
|
6
|
+
function buildContextSummary(r) {
|
|
7
|
+
const parts = [];
|
|
8
|
+
if (r.narrativeContext?.focus) {
|
|
9
|
+
parts.push(`what=${r.narrativeContext.focus}`);
|
|
10
|
+
}
|
|
11
|
+
if (r.relationshipContext?.tone) {
|
|
12
|
+
parts.push(`tone=${r.relationshipContext.tone}`);
|
|
13
|
+
}
|
|
14
|
+
if (r.relationshipContext?.topicAffinities && r.relationshipContext.topicAffinities.length > 0) {
|
|
15
|
+
parts.push(`interests=${r.relationshipContext.topicAffinities.join(",")}`);
|
|
16
|
+
}
|
|
17
|
+
return parts.length > 0 ? `;context=${parts.join(";")}` : "";
|
|
18
|
+
}
|
|
6
19
|
function baseDraftText(request) {
|
|
7
20
|
const ids = request.sourceRefs.map((s) => s.id).join(",");
|
|
8
|
-
return `draft:${request.candidateId}:grounded:${ids}`;
|
|
21
|
+
return `draft:${request.candidateId}:grounded:${ids}${buildContextSummary(request)}`;
|
|
9
22
|
}
|
|
10
23
|
export async function draftOutreachMessage(request) {
|
|
11
24
|
const parsed = safeParseOutreachDraftRequest(request);
|