@haaaiawd/second-nature 0.1.22 → 0.1.24

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.
Files changed (79) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/runtime/cli/commands/connector-init.d.ts +19 -0
  4. package/runtime/cli/commands/connector-init.js +168 -0
  5. package/runtime/cli/commands/connector-status.d.ts +12 -0
  6. package/runtime/cli/commands/connector-status.js +156 -0
  7. package/runtime/cli/commands/index.js +40 -0
  8. package/runtime/cli/index.js +52 -0
  9. package/runtime/cli/ops/ops-router.d.ts +5 -0
  10. package/runtime/cli/ops/ops-router.js +34 -0
  11. package/runtime/cli/ops/workspace-heartbeat-runner.js +22 -0
  12. package/runtime/connectors/agent-network/agent-world/adapter.d.ts +11 -0
  13. package/runtime/connectors/agent-network/agent-world/adapter.js +58 -0
  14. package/runtime/connectors/agent-network/agent-world/index.d.ts +2 -0
  15. package/runtime/connectors/agent-network/agent-world/index.js +2 -0
  16. package/runtime/connectors/agent-network/agent-world/manifest.d.ts +2 -0
  17. package/runtime/connectors/agent-network/agent-world/manifest.js +7 -0
  18. package/runtime/connectors/base/manifest.d.ts +13 -0
  19. package/runtime/connectors/base/manifest.js +47 -0
  20. package/runtime/connectors/manifest/manifest-parser.d.ts +16 -0
  21. package/runtime/connectors/manifest/manifest-parser.js +35 -0
  22. package/runtime/connectors/manifest/manifest-schema.d.ts +145 -0
  23. package/runtime/connectors/manifest/manifest-schema.js +51 -0
  24. package/runtime/connectors/registry/dynamic-connector-registry.d.ts +29 -0
  25. package/runtime/connectors/registry/dynamic-connector-registry.js +123 -0
  26. package/runtime/connectors/registry/index.d.ts +3 -0
  27. package/runtime/connectors/registry/index.js +3 -0
  28. package/runtime/connectors/registry/manifest-scanner.d.ts +9 -0
  29. package/runtime/connectors/registry/manifest-scanner.js +29 -0
  30. package/runtime/connectors/registry/trust-policy.d.ts +13 -0
  31. package/runtime/connectors/registry/trust-policy.js +37 -0
  32. package/runtime/connectors/services/connector-executor-adapter.js +49 -0
  33. package/runtime/core/second-nature/heartbeat/heartbeat-loop.d.ts +3 -0
  34. package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +52 -1
  35. package/runtime/core/second-nature/heartbeat/snapshot-builder.d.ts +3 -0
  36. package/runtime/core/second-nature/orchestrator/goal-priority.d.ts +19 -0
  37. package/runtime/core/second-nature/orchestrator/goal-priority.js +67 -0
  38. package/runtime/core/second-nature/orchestrator/intent-planner.js +8 -0
  39. package/runtime/core/second-nature/orchestrator/narrative-update.d.ts +27 -0
  40. package/runtime/core/second-nature/orchestrator/narrative-update.js +107 -0
  41. package/runtime/core/second-nature/types.d.ts +4 -0
  42. package/runtime/guidance/draft-narrative-outreach.d.ts +36 -0
  43. package/runtime/guidance/draft-narrative-outreach.js +84 -0
  44. package/runtime/guidance/index.d.ts +1 -0
  45. package/runtime/guidance/index.js +1 -0
  46. package/runtime/guidance/outreach-draft-schema.d.ts +3 -3
  47. package/runtime/observability/connector-inventory-ledger.d.ts +45 -0
  48. package/runtime/observability/connector-inventory-ledger.js +72 -0
  49. package/runtime/observability/db/index.js +13 -0
  50. package/runtime/observability/db/schema/connector-inventory.d.ts +174 -0
  51. package/runtime/observability/db/schema/connector-inventory.js +15 -0
  52. package/runtime/observability/db/schema/index.d.ts +1 -0
  53. package/runtime/observability/db/schema/index.js +1 -0
  54. package/runtime/storage/chronicle/session-chronicle-store.d.ts +42 -0
  55. package/runtime/storage/chronicle/session-chronicle-store.js +66 -0
  56. package/runtime/storage/db/index.js +75 -0
  57. package/runtime/storage/db/schema/agent-goal.d.ts +235 -0
  58. package/runtime/storage/db/schema/agent-goal.js +19 -0
  59. package/runtime/storage/db/schema/index.d.ts +5 -0
  60. package/runtime/storage/db/schema/index.js +5 -0
  61. package/runtime/storage/db/schema/memory-store.d.ts +199 -0
  62. package/runtime/storage/db/schema/memory-store.js +18 -0
  63. package/runtime/storage/db/schema/narrative-state.d.ts +195 -0
  64. package/runtime/storage/db/schema/narrative-state.js +16 -0
  65. package/runtime/storage/db/schema/relationship-memory.d.ts +174 -0
  66. package/runtime/storage/db/schema/relationship-memory.js +14 -0
  67. package/runtime/storage/db/schema/session-chronicle.d.ts +199 -0
  68. package/runtime/storage/db/schema/session-chronicle.js +18 -0
  69. package/runtime/storage/goal/agent-goal-store.d.ts +57 -0
  70. package/runtime/storage/goal/agent-goal-store.js +109 -0
  71. package/runtime/storage/index.d.ts +5 -0
  72. package/runtime/storage/index.js +5 -0
  73. package/runtime/storage/memory-store/memory-store-lifecycle.d.ts +70 -0
  74. package/runtime/storage/memory-store/memory-store-lifecycle.js +113 -0
  75. package/runtime/storage/narrative/narrative-state-store.d.ts +40 -0
  76. package/runtime/storage/narrative/narrative-state-store.js +79 -0
  77. package/runtime/storage/relationship/relationship-memory-store.d.ts +42 -0
  78. package/runtime/storage/relationship/relationship-memory-store.js +76 -0
  79. package/workspace-ops-bridge.js +1 -0
@@ -5,8 +5,10 @@ import { createConnectorPolicyLayer } from "../base/policy-layer.js";
5
5
  import { InMemoryEffectCommitLedger } from "../base/execution-policy.js";
6
6
  import { moltbookManifest } from "../social-community/moltbook/manifest.js";
7
7
  import { evomapManifest } from "../agent-network/evomap/manifest.js";
8
+ import { agentWorldManifest } from "../agent-network/agent-world/manifest.js";
8
9
  import { createMoltbookApiClient } from "../social-community/moltbook/api-client.js";
9
10
  import { createMoltbookRunner } from "../social-community/moltbook/adapter.js";
11
+ import { createAgentWorldRunner } from "../agent-network/agent-world/adapter.js";
10
12
  import { ExecutionTelemetry } from "../../observability/services/execution-telemetry.js";
11
13
  import { createCredentialVault } from "../../storage/services/credential-vault.js";
12
14
  import { createCredentialRouteContextPort } from "./credential-route-context.js";
@@ -74,6 +76,52 @@ function createAdaptiveExecutionRunner(vault) {
74
76
  },
75
77
  };
76
78
  }
79
+ if (platformId === "agent-world") {
80
+ const baseUrl = process.env.SECOND_NATURE_AGENT_WORLD_BASE_URL;
81
+ if (!baseUrl) {
82
+ return {
83
+ platformId,
84
+ channel: request.preferredChannel ?? "api_rest",
85
+ latencyMs: Date.now() - started,
86
+ success: false,
87
+ error: {
88
+ code: "configuration_missing",
89
+ detail: "SECOND_NATURE_AGENT_WORLD_BASE_URL not set",
90
+ },
91
+ };
92
+ }
93
+ const runner = createAgentWorldRunner({
94
+ apiClient: {
95
+ async readFeed(payload, _apiKey) {
96
+ const resp = await fetch(`${baseUrl}/api/v1/feed`, {
97
+ headers: { "Authorization": `Bearer ${_apiKey}`, "Content-Type": "application/json" },
98
+ });
99
+ if (!resp.ok)
100
+ throw { code: "api_error", detail: `agent-world feed: ${resp.status}` };
101
+ return resp.json();
102
+ },
103
+ async discoverWork(payload, _apiKey) {
104
+ const resp = await fetch(`${baseUrl}/api/v1/work`, {
105
+ headers: { "Authorization": `Bearer ${_apiKey}`, "Content-Type": "application/json" },
106
+ });
107
+ if (!resp.ok)
108
+ throw { code: "api_error", detail: `agent-world work: ${resp.status}` };
109
+ return resp.json();
110
+ },
111
+ async claimTask(payload, _apiKey) {
112
+ const resp = await fetch(`${baseUrl}/api/v1/tasks/${payload.taskId ?? "unknown"}/claim`, {
113
+ method: "POST",
114
+ headers: { "Authorization": `Bearer ${_apiKey}`, "Content-Type": "application/json" },
115
+ body: JSON.stringify(payload),
116
+ });
117
+ if (!resp.ok)
118
+ throw { code: "api_error", detail: `agent-world claim: ${resp.status}` };
119
+ return resp.json();
120
+ },
121
+ },
122
+ });
123
+ return runner.run(_plan, request);
124
+ }
77
125
  return {
78
126
  platformId,
79
127
  channel: request.preferredChannel ?? "api_rest",
@@ -92,6 +140,7 @@ export function createConnectorExecutorAdapter(options) {
92
140
  const registry = new CapabilityContractRegistry();
93
141
  registry.register({ ...moltbookManifest });
94
142
  registry.register({ ...evomapManifest });
143
+ registry.register({ ...agentWorldManifest });
95
144
  const routeContextPort = createCredentialRouteContextPort(vault);
96
145
  const routePlanner = new ConnectorRoutePlanner(registry, routeContextPort, new ChannelHealthStore());
97
146
  const telemetry = new ExecutionTelemetry(options.observabilityDb);
@@ -20,6 +20,7 @@ import type { GuidanceDraftPort } from "../../../guidance/outreach-draft-schema.
20
20
  import type { StateDatabase } from "../../../storage/db/index.js";
21
21
  import { type OpenClawDeliveryPort } from "../outreach/dispatch-user-outreach.js";
22
22
  import type { ConnectorExecutor } from "../../../connectors/base/contract.js";
23
+ import type { NarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
23
24
  export interface HeartbeatDecisionTracePayload {
24
25
  scope: RuntimeScope;
25
26
  status: HeartbeatCycleStatus;
@@ -58,6 +59,8 @@ export interface HeartbeatDeps {
58
59
  * through the connector-system instead of returning connector_dispatch_unwired.
59
60
  */
60
61
  connectorExecutor?: ConnectorExecutor;
62
+ /** T2.1.5: when present, heartbeat writes a source-backed NarrativeState revision after each cycle. */
63
+ narrativeStateStore?: NarrativeStateStore;
61
64
  }
62
65
  /**
63
66
  * Ingest a heartbeat rhythm signal and drive one full decision round.
@@ -1,11 +1,13 @@
1
1
  import { buildContinuitySnapshot, } from "./snapshot-builder.js";
2
2
  import { buildHeartbeatRuntimeSnapshot, } from "./runtime-snapshot.js";
3
3
  import { planCandidateIntents } from "../orchestrator/intent-planner.js";
4
+ import { applyGoalPriority } from "../orchestrator/goal-priority.js";
4
5
  import { evaluateHardGuards } from "../orchestrator/guard-layer.js";
5
6
  import { dispatchUserOutreachIntent, } from "../outreach/dispatch-user-outreach.js";
6
7
  import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-from-snapshot.js";
7
8
  import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
8
9
  import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
10
+ import { updateNarrativeAfterEffect } from "../orchestrator/narrative-update.js";
9
11
  /**
10
12
  * Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
11
13
  * Exported for unit tests (CR-M1 wiring).
@@ -83,6 +85,49 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
83
85
  reasons,
84
86
  };
85
87
  }
88
+ /**
89
+ * T2.1.5: after the cycle result is known, write a narrative revision when
90
+ * a NarrativeStateStore is wired. Errors are swallowed so the cycle result
91
+ * is never blocked by a store failure. Store failures are optionally traced
92
+ * via recordDecisionTrace so operators can monitor store health.
93
+ */
94
+ async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store, recordTrace, signal) {
95
+ if (!store)
96
+ return;
97
+ try {
98
+ const prior = await store.loadNarrativeState();
99
+ const update = updateNarrativeAfterEffect({
100
+ result,
101
+ selectedIntent,
102
+ lifeEvidence: runtime.lifeEvidence,
103
+ priorNarrative: prior,
104
+ });
105
+ await store.updateNarrativeState(update);
106
+ }
107
+ catch {
108
+ // degrade silently; narrative update is best-effort
109
+ if (recordTrace && signal) {
110
+ try {
111
+ await recordTrace({
112
+ scope: result.scope,
113
+ status: result.status,
114
+ reasons: ["narrative_update_failed"],
115
+ selectedIntentId: selectedIntent?.id,
116
+ rhythmWindowId: runtime.rhythmWindow.windowId,
117
+ allowedIntentKinds: [...runtime.rhythmWindow.allowedIntentKinds],
118
+ candidateCount: 0,
119
+ lifeEvidenceEmpty: runtime.lifeEvidence.evidenceRefs.length === 0 &&
120
+ runtime.lifeEvidence.platformEventCount === 0 &&
121
+ runtime.lifeEvidence.workEventCount === 0,
122
+ trigger: signal.trigger,
123
+ });
124
+ }
125
+ catch {
126
+ // trace emission must also not block the cycle
127
+ }
128
+ }
129
+ }
130
+ }
86
131
  /**
87
132
  * Ingest a heartbeat rhythm signal and drive one full decision round.
88
133
  */
@@ -91,7 +136,8 @@ export async function ingestRhythmSignal(signal, deps) {
91
136
  const snapshot = buildContinuitySnapshot(inputs);
92
137
  const timestamp = signal.payload.timestamp;
93
138
  const runtime = buildHeartbeatRuntimeSnapshot(timestamp, inputs, snapshot);
94
- const candidates = planCandidateIntents(runtime);
139
+ const rawCandidates = planCandidateIntents(runtime);
140
+ const { candidates } = applyGoalPriority(rawCandidates, inputs.acceptedGoals);
95
141
  const emitTrace = async (result) => {
96
142
  if (!deps.recordDecisionTrace)
97
143
  return;
@@ -132,6 +178,7 @@ export async function ingestRhythmSignal(signal, deps) {
132
178
  ? { ...resolved, reasons: evaluation.reasons }
133
179
  : resolved;
134
180
  await emitTrace(result);
181
+ await maybeUpdateNarrativeState(result, intent, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
135
182
  return result;
136
183
  }
137
184
  if (evaluation.verdict === "defer") {
@@ -149,6 +196,7 @@ export async function ingestRhythmSignal(signal, deps) {
149
196
  reasons: ["silent_no_candidates"],
150
197
  };
151
198
  await emitTrace(result);
199
+ await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
152
200
  return result;
153
201
  }
154
202
  if (!anyAllow && anyDefer && !anyDeny) {
@@ -158,6 +206,7 @@ export async function ingestRhythmSignal(signal, deps) {
158
206
  reasons: denyReasons.length > 0 ? denyReasons : ["all_candidates_deferred"],
159
207
  };
160
208
  await emitTrace(result);
209
+ await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
161
210
  return result;
162
211
  }
163
212
  if (!anyAllow && denyReasons.length > 0) {
@@ -167,6 +216,7 @@ export async function ingestRhythmSignal(signal, deps) {
167
216
  reasons: denyReasons,
168
217
  };
169
218
  await emitTrace(result);
219
+ await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
170
220
  return result;
171
221
  }
172
222
  const result = {
@@ -175,6 +225,7 @@ export async function ingestRhythmSignal(signal, deps) {
175
225
  reasons: ["no_allow_verdict"],
176
226
  };
177
227
  await emitTrace(result);
228
+ await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
178
229
  return result;
179
230
  }
180
231
  /**
@@ -10,6 +10,7 @@ import type { ContinuitySnapshot, ControlPlaneSourceRef, TopLevelMode } from "..
10
10
  import type { RhythmPolicy } from "../rhythm/rhythm-policy.js";
11
11
  import type { DeliveryCapabilitySnapshot } from "../outreach/delivery-target.js";
12
12
  import type { UserInterestSnapshot } from "../../../storage/user-interest/types.js";
13
+ import type { AgentGoal } from "../../../storage/goal/agent-goal-store.js";
13
14
  export interface SnapshotInputs {
14
15
  mode: TopLevelMode;
15
16
  currentWindowId: string;
@@ -41,6 +42,8 @@ export interface SnapshotInputs {
41
42
  deliveryCapability?: DeliveryCapabilitySnapshot;
42
43
  /** When present, outreach judgment uses this user-interest read model (T4.2.2). */
43
44
  userInterestSnapshot?: UserInterestSnapshot;
45
+ /** T2.1.4: accepted goals to influence candidate intent priority. */
46
+ acceptedGoals?: AgentGoal[];
44
47
  }
45
48
  /**
46
49
  * Build a ContinuitySnapshot from loaded inputs.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * T2.1.4 — Goal-Directed Intent Priority.
3
+ *
4
+ * `applyGoalPriority` adjusts candidate intent priorities based on accepted AgentGoals.
5
+ * Priority order: user_task > accepted_goal > rhythm.
6
+ * Only goals with status === "accepted" and origin !== "agent_proposed" are considered.
7
+ * All other statuses (proposal / rejected / completed / paused) are implicitly excluded.
8
+ */
9
+ import type { CandidateIntent } from "../types.js";
10
+ import type { AgentGoal } from "../../../storage/goal/agent-goal-store.js";
11
+ export interface ApplyGoalPriorityResult {
12
+ candidates: CandidateIntent[];
13
+ goalInfluences: Array<{
14
+ candidateId: string;
15
+ goalIds: string[];
16
+ boost: number;
17
+ }>;
18
+ }
19
+ export declare function applyGoalPriority(candidates: CandidateIntent[], goals: AgentGoal[] | undefined): ApplyGoalPriorityResult;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Per-goal priority boost applied when an accepted goal matches a candidate.
3
+ *
4
+ * Rationale: +20 per goal keeps a priority-50 candidate under 200
5
+ * even with 7 matching goals (50 + 140 = 190). Planner baselines
6
+ * range from 40–100, so 200 provides ample headroom without overflow.
7
+ */
8
+ const GOAL_PRIORITY_BOOST = 20;
9
+ function isGoalRelatedToCandidate(goal, candidate) {
10
+ if (!candidate.platformId)
11
+ return false;
12
+ const goalText = `${goal.description} ${goal.completionCriteria}`.toLowerCase();
13
+ const platformId = candidate.platformId.toLowerCase();
14
+ // Direct platformId mention in goal text
15
+ if (goalText.includes(platformId))
16
+ return true;
17
+ // Goal description contains candidate summary keywords
18
+ const summaryWords = candidate.summary.toLowerCase().split(/\s+/);
19
+ for (const word of summaryWords) {
20
+ if (word.length > 3 && goalText.includes(word))
21
+ return true;
22
+ }
23
+ return false;
24
+ }
25
+ export function applyGoalPriority(candidates, goals) {
26
+ const acceptedGoals = (goals ?? []).filter((g) => g.status === "accepted" && g.origin !== "agent_proposed");
27
+ if (acceptedGoals.length === 0) {
28
+ return {
29
+ candidates: candidates.map((c) => ({
30
+ ...c,
31
+ priorityReasons: c.priorityReasons ?? ["rhythm"],
32
+ })),
33
+ goalInfluences: [],
34
+ };
35
+ }
36
+ const influences = [];
37
+ const adjusted = candidates.map((candidate) => {
38
+ const relatedGoals = acceptedGoals.filter((g) => isGoalRelatedToCandidate(g, candidate));
39
+ if (relatedGoals.length === 0) {
40
+ return {
41
+ ...candidate,
42
+ priorityReasons: candidate.priorityReasons ?? ["rhythm"],
43
+ };
44
+ }
45
+ const boost = GOAL_PRIORITY_BOOST * relatedGoals.length;
46
+ const goalIds = relatedGoals.map((g) => g.goalId);
47
+ influences.push({
48
+ candidateId: candidate.id,
49
+ goalIds,
50
+ boost,
51
+ });
52
+ const reasons = [
53
+ ...(candidate.priorityReasons ?? ["rhythm"]),
54
+ ...relatedGoals.map((g) => `goal_boost:${g.goalId}`),
55
+ ];
56
+ return {
57
+ ...candidate,
58
+ priority: candidate.priority + boost,
59
+ goalInfluenceRefs: goalIds,
60
+ priorityReasons: reasons,
61
+ };
62
+ });
63
+ return {
64
+ candidates: adjusted.sort((a, b) => b.priority - a.priority),
65
+ goalInfluences: influences,
66
+ };
67
+ }
@@ -34,6 +34,7 @@ function planWorkIntents(runtime) {
34
34
  effectClass: "connector_action",
35
35
  sourceRefs: [...OBLIGATION_SOURCE],
36
36
  idempotencyKey: `obligation:${obligation}:${index}`,
37
+ goalInfluenceRefs: [],
37
38
  }));
38
39
  }
39
40
  function planExplorationIntents(runtime) {
@@ -50,6 +51,7 @@ function planExplorationIntents(runtime) {
50
51
  effectClass: "connector_action",
51
52
  sourceRefs: refs,
52
53
  idempotencyKey: "exploration:scan platform opportunities",
54
+ goalInfluenceRefs: [],
53
55
  },
54
56
  ];
55
57
  }
@@ -67,6 +69,7 @@ function planSocialIntents(runtime) {
67
69
  effectClass: "connector_action",
68
70
  sourceRefs: refs,
69
71
  idempotencyKey: "social:engage social platforms",
72
+ goalInfluenceRefs: [],
70
73
  },
71
74
  ];
72
75
  }
@@ -85,6 +88,7 @@ function planQuietReflectionIntents(runtime) {
85
88
  effectClass: "no_effect",
86
89
  sourceRefs: [],
87
90
  idempotencyKey: "quiet:bookkeeping",
91
+ goalInfluenceRefs: [],
88
92
  });
89
93
  }
90
94
  if (isAllowedKind("maintenance", runtime)) {
@@ -97,6 +101,7 @@ function planQuietReflectionIntents(runtime) {
97
101
  effectClass: "maintenance",
98
102
  sourceRefs: [],
99
103
  idempotencyKey: "maintenance:checks",
104
+ goalInfluenceRefs: [],
100
105
  });
101
106
  }
102
107
  if (isAllowedKind("reflection", runtime)) {
@@ -110,6 +115,7 @@ function planQuietReflectionIntents(runtime) {
110
115
  effectClass: "narrative_reflection",
111
116
  sourceRefs: refs,
112
117
  idempotencyKey: "reflection:narrative",
118
+ goalInfluenceRefs: [],
113
119
  });
114
120
  }
115
121
  return out;
@@ -131,6 +137,7 @@ function planOutreachIntents(runtime) {
131
137
  effectClass: "user_outreach",
132
138
  sourceRefs: refs,
133
139
  idempotencyKey: "outreach:consider proactive user outreach",
140
+ goalInfluenceRefs: [],
134
141
  },
135
142
  ];
136
143
  }
@@ -149,6 +156,7 @@ export function planCandidateIntents(runtime) {
149
156
  effectClass: "maintenance",
150
157
  sourceRefs: [],
151
158
  idempotencyKey: "maintenance:checks",
159
+ goalInfluenceRefs: [],
152
160
  },
153
161
  ];
154
162
  return pausedMaintenance
@@ -0,0 +1,27 @@
1
+ /**
2
+ * T2.1.5 — NarrativeState update after heartbeat effect/fallback.
3
+ *
4
+ * Produces a source-backed narrative revision or an honest empty-state
5
+ * (`awaiting_sources` / `insufficient_sources`) based on the cycle result.
6
+ *
7
+ * Rules-only implementation; no live LLM.
8
+ */
9
+ import type { HeartbeatCycleResult } from "../heartbeat/signal.js";
10
+ import type { CandidateIntent } from "../types.js";
11
+ import type { PlannerLifeEvidenceSlice } from "../heartbeat/runtime-snapshot.js";
12
+ import type { NarrativeState, NarrativeStateUpdate } from "../../../storage/narrative/narrative-state-store.js";
13
+ export interface UpdateNarrativeAfterEffectInput {
14
+ result: HeartbeatCycleResult;
15
+ selectedIntent?: CandidateIntent;
16
+ lifeEvidence: PlannerLifeEvidenceSlice;
17
+ priorNarrative?: NarrativeState | null;
18
+ }
19
+ /**
20
+ * Build the next NarrativeState revision from a completed heartbeat cycle.
21
+ *
22
+ * - `intent_selected` + sources → `active`, focus/progress updated.
23
+ * - `intent_selected` without sources → `awaiting_sources`, claim recorded as unsupported.
24
+ * - No action (heartbeat_ok / denied / deferred / etc.) → preserve prior state or
25
+ * seed an empty `awaiting_sources` state when no prior exists.
26
+ */
27
+ export declare function updateNarrativeAfterEffect(input: UpdateNarrativeAfterEffectInput): NarrativeStateUpdate;
@@ -0,0 +1,107 @@
1
+ const MAX_PROGRESS_ENTRIES = 10;
2
+ const DEFAULT_NARRATIVE_ID = "default";
3
+ function mapControlPlaneRefToSourceRef(ref) {
4
+ return {
5
+ sourceId: ref.id,
6
+ kind: ref.kind,
7
+ url: ref.uri,
8
+ snippet: ref.excerptHash,
9
+ };
10
+ }
11
+ /**
12
+ * Compute narrative confidence based on source evidence.
13
+ *
14
+ * Formula: min(intentSources / 3, 1) + (lifeEvidenceSources > 0 ? 0.1 : 0)
15
+ *
16
+ * Rationale:
17
+ * - Base: 1/3 per intent source (3 sources = 100% confidence)
18
+ * - Boost: +0.1 if any life evidence exists (signals corroboration)
19
+ * - Capped at 1.0 (100%)
20
+ */
21
+ function computeConfidence(intentSources, lifeEvidenceSources) {
22
+ if (intentSources === 0 && lifeEvidenceSources === 0)
23
+ return 0;
24
+ const base = Math.min(intentSources / 3, 1);
25
+ const boost = lifeEvidenceSources > 0 ? 0.1 : 0;
26
+ return Math.min(base + boost, 1);
27
+ }
28
+ /**
29
+ * Build the next NarrativeState revision from a completed heartbeat cycle.
30
+ *
31
+ * - `intent_selected` + sources → `active`, focus/progress updated.
32
+ * - `intent_selected` without sources → `awaiting_sources`, claim recorded as unsupported.
33
+ * - No action (heartbeat_ok / denied / deferred / etc.) → preserve prior state or
34
+ * seed an empty `awaiting_sources` state when no prior exists.
35
+ */
36
+ export function updateNarrativeAfterEffect(input) {
37
+ const { result, selectedIntent, lifeEvidence, priorNarrative } = input;
38
+ const now = new Date().toISOString();
39
+ const prior = priorNarrative ?? null;
40
+ const narrativeId = prior?.narrativeId ?? DEFAULT_NARRATIVE_ID;
41
+ const nextRevision = (prior?.revision ?? 0) + 1;
42
+ // --- intent_selected branch ------------------------------------------------
43
+ if (result.status === "intent_selected" && selectedIntent) {
44
+ const hasIntentSources = selectedIntent.sourceRefs.length > 0;
45
+ const hasLifeEvidence = !(lifeEvidence.evidenceRefs.length === 0 &&
46
+ lifeEvidence.platformEventCount === 0 &&
47
+ lifeEvidence.workEventCount === 0);
48
+ const sourceRefs = selectedIntent.sourceRefs.map(mapControlPlaneRefToSourceRef);
49
+ if (hasIntentSources || hasLifeEvidence) {
50
+ // Source-backed revision
51
+ const progressEntry = `${selectedIntent.effectClass}: ${selectedIntent.summary}`;
52
+ const progress = [...(prior?.progress ?? [])];
53
+ if (!progress.includes(progressEntry)) {
54
+ progress.push(progressEntry);
55
+ }
56
+ const boundedProgress = progress.slice(-MAX_PROGRESS_ENTRIES);
57
+ return {
58
+ narrativeId,
59
+ revision: nextRevision,
60
+ focus: selectedIntent.summary,
61
+ progress: boundedProgress,
62
+ nextIntent: "continue",
63
+ confidence: computeConfidence(selectedIntent.sourceRefs.length, lifeEvidence.evidenceRefs.length),
64
+ sourceRefs,
65
+ unsupportedClaims: [],
66
+ status: "active",
67
+ updatedAt: now,
68
+ };
69
+ }
70
+ // No sources → awaiting_sources
71
+ return {
72
+ narrativeId,
73
+ revision: nextRevision,
74
+ focus: prior?.focus ?? "awaiting_evidence",
75
+ progress: prior?.progress ?? [],
76
+ nextIntent: "await_sources",
77
+ confidence: 0,
78
+ sourceRefs: [],
79
+ unsupportedClaims: [selectedIntent.summary],
80
+ status: "awaiting_sources",
81
+ updatedAt: now,
82
+ };
83
+ }
84
+ // --- No action / fallback branches -----------------------------------------
85
+ if (prior) {
86
+ // Preserve existing state, bump revision and timestamp so observers
87
+ // know the heartbeat cycle ran even when no intent was selected.
88
+ return {
89
+ ...prior,
90
+ revision: nextRevision,
91
+ updatedAt: now,
92
+ };
93
+ }
94
+ // No prior state and no action → seed empty awaiting_sources state
95
+ return {
96
+ narrativeId,
97
+ revision: nextRevision,
98
+ focus: "awaiting_evidence",
99
+ progress: [],
100
+ nextIntent: "await_sources",
101
+ confidence: 0,
102
+ sourceRefs: [],
103
+ unsupportedClaims: [],
104
+ status: "awaiting_sources",
105
+ updatedAt: now,
106
+ };
107
+ }
@@ -41,6 +41,10 @@ export interface CandidateIntent {
41
41
  sourceRefs: ControlPlaneSourceRef[];
42
42
  /** Dedupe / cooldown key; defaults to stable fingerprint in guard layer when omitted. */
43
43
  idempotencyKey?: string;
44
+ /** T2.1.4: IDs of accepted AgentGoals that influenced this candidate's priority. */
45
+ goalInfluenceRefs?: string[];
46
+ /** T2.1.4: Human-readable reasons for the priority value (goal influence, user task, rhythm). */
47
+ priorityReasons?: string[];
44
48
  }
45
49
  export interface GuardEvaluation {
46
50
  verdict: GuardVerdict;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * T6.1.1 — Narrative Outreach Draft generator.
3
+ *
4
+ * Produces source-backed outreach drafts from evidence + narrative + relationship.
5
+ * Rules-only implementation (no live LLM); honors grounding, redaction, and insufficient_history.
6
+ *
7
+ * NOTE: This function assumes narrativeState.focus / nextIntent and
8
+ * relationshipMemory fields are pre-redacted by upstream state-system.
9
+ * No additional PII filtering is applied here — defense-in-depth is
10
+ * the responsibility of the NarrativeState / RelationshipMemory stores.
11
+ */
12
+ import type { NarrativeState, SourceRef } from "../storage/narrative/narrative-state-store.js";
13
+ import type { RelationshipMemory } from "../storage/relationship/relationship-memory-store.js";
14
+ export interface NarrativeOutreachDraftRequest {
15
+ evidenceRefs: SourceRef[];
16
+ narrativeState?: NarrativeState;
17
+ relationshipMemory?: RelationshipMemory;
18
+ platformId?: string;
19
+ }
20
+ export interface GroundingReport {
21
+ status: "grounded" | "degraded" | "blocked";
22
+ sourceCoverage: number;
23
+ unsupportedClaims: string[];
24
+ reason: string;
25
+ }
26
+ export interface NarrativeOutreachDraftResult {
27
+ draft: {
28
+ whatHappened: string;
29
+ whyItMatters: string;
30
+ sourceRefs: SourceRef[];
31
+ tone: "friend" | "insufficient_history" | "blocked";
32
+ };
33
+ groundingReport: GroundingReport;
34
+ promptVersion: string;
35
+ }
36
+ export declare function draftNarrativeOutreach(request: NarrativeOutreachDraftRequest): NarrativeOutreachDraftResult;
@@ -0,0 +1,84 @@
1
+ function computeSourceCoverage(claims, _evidenceRefs, narrativeSourceRefs) {
2
+ if (claims.length === 0)
3
+ return { coverage: 1, unsupported: [] };
4
+ // Only narrative-level source refs count toward coverage.
5
+ const count = narrativeSourceRefs?.length ?? 0;
6
+ const coverage = Math.min(count / claims.length, 1);
7
+ return {
8
+ coverage,
9
+ unsupported: coverage >= 1 ? [] : claims,
10
+ };
11
+ }
12
+ export function draftNarrativeOutreach(request) {
13
+ const { evidenceRefs, narrativeState, relationshipMemory, platformId } = request;
14
+ // T6.1.1 — Block when source refs are missing
15
+ if (!evidenceRefs || evidenceRefs.length === 0) {
16
+ return {
17
+ draft: {
18
+ whatHappened: "",
19
+ whyItMatters: "",
20
+ sourceRefs: [],
21
+ tone: "blocked",
22
+ },
23
+ groundingReport: {
24
+ status: "blocked",
25
+ sourceCoverage: 0,
26
+ unsupportedClaims: ["missing_evidence_refs"],
27
+ reason: "no_source_refs_provided",
28
+ },
29
+ promptVersion: "v6-rules-only-1.0",
30
+ };
31
+ }
32
+ // Build claims from narrative state
33
+ const claims = [];
34
+ if (narrativeState?.focus) {
35
+ claims.push(narrativeState.focus);
36
+ }
37
+ if (narrativeState?.progress && narrativeState.progress.length > 0) {
38
+ claims.push(...narrativeState.progress);
39
+ }
40
+ const { coverage, unsupported } = computeSourceCoverage(claims, evidenceRefs, narrativeState?.sourceRefs);
41
+ // T6.1.1 — Degrade when source coverage is low
42
+ if (coverage < 0.5) {
43
+ return {
44
+ draft: {
45
+ whatHappened: narrativeState?.focus ?? "",
46
+ whyItMatters: "",
47
+ sourceRefs: evidenceRefs,
48
+ tone: "insufficient_history",
49
+ },
50
+ groundingReport: {
51
+ status: "degraded",
52
+ sourceCoverage: coverage,
53
+ unsupportedClaims: unsupported,
54
+ reason: "insufficient_source_coverage",
55
+ },
56
+ promptVersion: "v6-rules-only-1.0",
57
+ };
58
+ }
59
+ // T6.1.1 — insufficient_history tone when relationship is weak
60
+ const avgAffinity = relationshipMemory && relationshipMemory.topicAffinities.length > 0
61
+ ? relationshipMemory.topicAffinities.reduce((s, t) => s + t.affinity, 0) /
62
+ relationshipMemory.topicAffinities.length
63
+ : 0;
64
+ const tone = avgAffinity < 0.3 ? "insufficient_history" : "friend";
65
+ const whatHappened = narrativeState?.focus ?? "Recent activity observed.";
66
+ const whyItMatters = narrativeState?.nextIntent
67
+ ? `This relates to the current focus: ${narrativeState.nextIntent}.`
68
+ : "This may be relevant to your interests.";
69
+ return {
70
+ draft: {
71
+ whatHappened,
72
+ whyItMatters,
73
+ sourceRefs: evidenceRefs,
74
+ tone,
75
+ },
76
+ groundingReport: {
77
+ status: "grounded",
78
+ sourceCoverage: coverage,
79
+ unsupportedClaims: unsupported,
80
+ reason: "source_backed",
81
+ },
82
+ promptVersion: "v6-rules-only-1.0",
83
+ };
84
+ }
@@ -9,3 +9,4 @@ export * from "./guidance-assembler.js";
9
9
  export * from "./outreach-draft-schema.js";
10
10
  export * from "./draft-outreach-message.js";
11
11
  export * from "./evidence-guidance.js";
12
+ export * from "./draft-narrative-outreach.js";
@@ -9,3 +9,4 @@ export * from "./guidance-assembler.js";
9
9
  export * from "./outreach-draft-schema.js";
10
10
  export * from "./draft-outreach-message.js";
11
11
  export * from "./evidence-guidance.js";
12
+ export * from "./draft-narrative-outreach.js";