@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T4.2.1 — Owner reply ingestion and RelationshipMemory feedback loop.
|
|
3
|
+
*
|
|
4
|
+
* When an owner replies to an outreach, this function:
|
|
5
|
+
* 1. Appends a `SessionChronicle` entry with the reply context.
|
|
6
|
+
* 2. Loads the current `RelationshipMemory`.
|
|
7
|
+
* 3. Infers tone/timing/topic from the reply text and updates the memory.
|
|
8
|
+
* 4. Persists the updated `RelationshipMemory` with source refs pointing to the chronicle entry.
|
|
9
|
+
*
|
|
10
|
+
* Boundaries:
|
|
11
|
+
* - Does NOT generate outreach drafts; that is the guidance layer's job.
|
|
12
|
+
* - Does NOT execute connectors; this is a pure state update path.
|
|
13
|
+
* - Errors in relationship update must not break chronicle write (chronicle is source of truth).
|
|
14
|
+
*/
|
|
15
|
+
import type { StateDatabase } from "../../../storage/db/index.js";
|
|
16
|
+
import { type OwnerReplySignal } from "../../../storage/chronicle/session-chronicle-store.js";
|
|
17
|
+
import { type RelationshipMemory, type TopicAffinity } from "../../../storage/relationship/relationship-memory-store.js";
|
|
18
|
+
export interface ReplyInferenceConfig {
|
|
19
|
+
positiveKeywords?: string[];
|
|
20
|
+
negativeKeywords?: string[];
|
|
21
|
+
busyKeywords?: string[];
|
|
22
|
+
topicPatterns?: Record<string, string[]>;
|
|
23
|
+
}
|
|
24
|
+
export declare function inferTone(text: string, config?: ReplyInferenceConfig): "casual" | "direct" | "quiet" | "unknown";
|
|
25
|
+
export declare function inferTiming(text: string, config?: ReplyInferenceConfig): "responsive" | "busy" | undefined;
|
|
26
|
+
export declare function inferTopics(text: string, config?: ReplyInferenceConfig): string[];
|
|
27
|
+
export declare function mergeTopicAffinities(existing: TopicAffinity[], newTopics: string[]): TopicAffinity[];
|
|
28
|
+
export interface ProcessOwnerReplyInput {
|
|
29
|
+
/** The raw reply text from the owner. */
|
|
30
|
+
replyText: string;
|
|
31
|
+
/** The decisionId of the outreach this reply is responding to. */
|
|
32
|
+
relatedDecisionId: string;
|
|
33
|
+
/** Optional explicit owner signal (parsed by host or explicit UI). */
|
|
34
|
+
explicitSignal?: OwnerReplySignal;
|
|
35
|
+
}
|
|
36
|
+
export interface ProcessOwnerReplyResult {
|
|
37
|
+
chronicleEntryId: string;
|
|
38
|
+
relationshipUpdated: boolean;
|
|
39
|
+
priorMemory?: RelationshipMemory;
|
|
40
|
+
updatedMemory?: RelationshipMemory;
|
|
41
|
+
relationshipUpdateError?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Process an owner reply: write chronicle, update RelationshipMemory.
|
|
45
|
+
*/
|
|
46
|
+
export declare function processOwnerReply(input: ProcessOwnerReplyInput, state: StateDatabase): Promise<ProcessOwnerReplyResult>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { createSessionChronicleStore, } from "../../../storage/chronicle/session-chronicle-store.js";
|
|
2
|
+
import { createRelationshipMemoryStore, } from "../../../storage/relationship/relationship-memory-store.js";
|
|
3
|
+
const DEFAULT_POSITIVE_KEYWORDS = [
|
|
4
|
+
"agree", "thanks", "appreciate", "helpful", "good", "great", "love", "like", "enjoy",
|
|
5
|
+
"excited", "happy", "nice", "wonderful", "awesome", "perfect", "cool", "ok", "yes",
|
|
6
|
+
];
|
|
7
|
+
const DEFAULT_NEGATIVE_KEYWORDS = [
|
|
8
|
+
"disagree", "frustrated", "annoying", "bad", "hate", "dislike", "angry", "upset",
|
|
9
|
+
"disappointed", "concerned", "no", "not", "never", "wrong", "terrible", "awful",
|
|
10
|
+
"useless", "stop", "don't",
|
|
11
|
+
];
|
|
12
|
+
const DEFAULT_BUSY_KEYWORDS = [
|
|
13
|
+
"busy", "swamped", "occupied", "tight schedule", "no time", "later",
|
|
14
|
+
"overloaded", "overwhelmed", "backlog", "not now", "another time", "schedule tight",
|
|
15
|
+
];
|
|
16
|
+
const DEFAULT_TOPIC_PATTERNS = {
|
|
17
|
+
work: ["work", "project", "task", "job", "delivery", "deadline"],
|
|
18
|
+
personal: ["family", "life", "health", "weekend", "trip"],
|
|
19
|
+
tech: ["code", "system", "bug", "feature", "architecture", "design"],
|
|
20
|
+
social: ["friend", "community", "meetup", "event", "collaboration"],
|
|
21
|
+
};
|
|
22
|
+
export function inferTone(text, config) {
|
|
23
|
+
const lower = text.toLowerCase();
|
|
24
|
+
const positiveKeywords = config?.positiveKeywords ?? DEFAULT_POSITIVE_KEYWORDS;
|
|
25
|
+
const negativeKeywords = config?.negativeKeywords ?? DEFAULT_NEGATIVE_KEYWORDS;
|
|
26
|
+
const pos = positiveKeywords.filter((w) => lower.includes(w)).length;
|
|
27
|
+
const neg = negativeKeywords.filter((w) => lower.includes(w)).length;
|
|
28
|
+
if (neg >= pos && neg > 0)
|
|
29
|
+
return "quiet"; // owner is negative → agent should be more reserved
|
|
30
|
+
if (pos > 0)
|
|
31
|
+
return "casual"; // positive → casual is fine
|
|
32
|
+
return "unknown";
|
|
33
|
+
}
|
|
34
|
+
export function inferTiming(text, config) {
|
|
35
|
+
const lower = text.toLowerCase();
|
|
36
|
+
const busyKeywords = config?.busyKeywords ?? DEFAULT_BUSY_KEYWORDS;
|
|
37
|
+
if (busyKeywords.some((w) => lower.includes(w)))
|
|
38
|
+
return "busy";
|
|
39
|
+
if (lower.includes("quick") || lower.includes("prompt"))
|
|
40
|
+
return "responsive";
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
export function inferTopics(text, config) {
|
|
44
|
+
const lower = text.toLowerCase();
|
|
45
|
+
const topicPatterns = config?.topicPatterns ?? DEFAULT_TOPIC_PATTERNS;
|
|
46
|
+
const topics = [];
|
|
47
|
+
for (const [topic, patterns] of Object.entries(topicPatterns)) {
|
|
48
|
+
if (patterns.some((p) => lower.includes(p))) {
|
|
49
|
+
topics.push(topic);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return topics;
|
|
53
|
+
}
|
|
54
|
+
export function mergeTopicAffinities(existing, newTopics) {
|
|
55
|
+
const map = new Map(existing.map((t) => [t.topic, t.affinity]));
|
|
56
|
+
for (const topic of newTopics) {
|
|
57
|
+
map.set(topic, Math.min(1, (map.get(topic) ?? 0) + 0.1));
|
|
58
|
+
}
|
|
59
|
+
return Array.from(map.entries())
|
|
60
|
+
.map(([topic, affinity]) => ({ topic, affinity }))
|
|
61
|
+
.sort((a, b) => b.affinity - a.affinity);
|
|
62
|
+
}
|
|
63
|
+
function redactSensitive(text) {
|
|
64
|
+
return text
|
|
65
|
+
.replace(/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g, "[REDACTED_CARD]")
|
|
66
|
+
.replace(/password[:\s=]+\S+/gi, "[REDACTED_PASSWORD]")
|
|
67
|
+
.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g, "[REDACTED_EMAIL]")
|
|
68
|
+
.replace(/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b/g, "[REDACTED_PHONE]")
|
|
69
|
+
.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[REDACTED_SSN]")
|
|
70
|
+
.replace(/\b(?:sk-|pk-|Bearer\s+|api[_-]?key[:\s=]+)[A-Za-z0-9_\-\/+=]{20,}\b/gi, "[REDACTED_TOKEN]");
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Process an owner reply: write chronicle, update RelationshipMemory.
|
|
74
|
+
*/
|
|
75
|
+
export async function processOwnerReply(input, state) {
|
|
76
|
+
const chronicleStore = createSessionChronicleStore(state);
|
|
77
|
+
const relStore = createRelationshipMemoryStore(state);
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
// 1. Write chronicle entry (source of truth)
|
|
80
|
+
const entryId = `owner_reply:${input.relatedDecisionId}:${Date.now()}`;
|
|
81
|
+
const replyText = input.replyText?.trim() ?? "";
|
|
82
|
+
const isEmpty = replyText.length === 0;
|
|
83
|
+
const tone = isEmpty ? "unknown" : inferTone(replyText);
|
|
84
|
+
const timing = isEmpty ? undefined : inferTiming(replyText);
|
|
85
|
+
const topics = isEmpty ? [] : inferTopics(replyText);
|
|
86
|
+
const chronicleEntry = {
|
|
87
|
+
entryId,
|
|
88
|
+
eventKind: "owner_reply",
|
|
89
|
+
actor: "owner",
|
|
90
|
+
occurredAt: now,
|
|
91
|
+
summary: redactSensitive(isEmpty ? "(empty reply)" : replyText.slice(0, 500)),
|
|
92
|
+
result: "succeeded",
|
|
93
|
+
sourceRefs: [{ sourceId: entryId, kind: "owner_reply", url: `chronicle://${entryId}` }],
|
|
94
|
+
relatedDecisionId: input.relatedDecisionId,
|
|
95
|
+
ownerReply: {
|
|
96
|
+
tone,
|
|
97
|
+
delayMinutes: input.explicitSignal?.delayMinutes,
|
|
98
|
+
topics: topics.length > 0 ? topics : input.explicitSignal?.topics,
|
|
99
|
+
explicitPreference: input.explicitSignal?.explicitPreference,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
await chronicleStore.appendSessionChronicle(chronicleEntry);
|
|
103
|
+
// 2. Load and update RelationshipMemory (best-effort)
|
|
104
|
+
let relationshipUpdated = false;
|
|
105
|
+
let priorMemory;
|
|
106
|
+
let updatedMemory;
|
|
107
|
+
try {
|
|
108
|
+
priorMemory = (await relStore.loadRelationshipMemory()) ?? undefined;
|
|
109
|
+
const nextRevision = (priorMemory?.revision ?? 0) + 1;
|
|
110
|
+
const topicAffinities = mergeTopicAffinities(priorMemory?.topicAffinities ?? [], topics);
|
|
111
|
+
const update = {
|
|
112
|
+
relationshipId: priorMemory?.relationshipId ?? "default",
|
|
113
|
+
revision: nextRevision,
|
|
114
|
+
tonePreference: tone !== "unknown" ? tone : (priorMemory?.tonePreference ?? "unknown"),
|
|
115
|
+
averageReplyDelayMinutes: input.explicitSignal?.delayMinutes ?? priorMemory?.averageReplyDelayMinutes,
|
|
116
|
+
noReplyCount: 0, // owner replied → reset counter
|
|
117
|
+
topicAffinities: topicAffinities.length > 0 ? topicAffinities : (priorMemory?.topicAffinities ?? []),
|
|
118
|
+
lastInteractionAt: now,
|
|
119
|
+
sourceRefs: [
|
|
120
|
+
...(priorMemory?.sourceRefs ?? []),
|
|
121
|
+
{ sourceId: entryId, kind: "owner_reply_feedback", url: `chronicle://${entryId}` },
|
|
122
|
+
],
|
|
123
|
+
updatedAt: now,
|
|
124
|
+
};
|
|
125
|
+
await relStore.upsertRelationshipMemory(update);
|
|
126
|
+
updatedMemory = (await relStore.loadRelationshipMemory()) ?? undefined;
|
|
127
|
+
relationshipUpdated = true;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// Relationship update is best-effort; chronicle is the source of truth.
|
|
131
|
+
// Missing memory update will be reflected in the next `explain relationship` query.
|
|
132
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
133
|
+
console.warn(`[owner-reply-feedback] RelationshipMemory update failed: ${errorMessage}`);
|
|
134
|
+
// Write a diagnostic chronicle entry so operators can trace the failure.
|
|
135
|
+
await chronicleStore.appendSessionChronicle({
|
|
136
|
+
entryId: `${entryId}:diagnostic`,
|
|
137
|
+
eventKind: "system_notice",
|
|
138
|
+
actor: "system",
|
|
139
|
+
occurredAt: new Date().toISOString(),
|
|
140
|
+
summary: `RelationshipMemory update failed: ${errorMessage}`,
|
|
141
|
+
result: "failed",
|
|
142
|
+
sourceRefs: [{ sourceId: entryId, kind: "owner_reply_feedback", url: `chronicle://${entryId}` }],
|
|
143
|
+
relatedDecisionId: input.relatedDecisionId,
|
|
144
|
+
});
|
|
145
|
+
return {
|
|
146
|
+
chronicleEntryId: entryId,
|
|
147
|
+
relationshipUpdated: false,
|
|
148
|
+
priorMemory,
|
|
149
|
+
updatedMemory,
|
|
150
|
+
relationshipUpdateError: errorMessage,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
chronicleEntryId: entryId,
|
|
155
|
+
relationshipUpdated,
|
|
156
|
+
priorMemory,
|
|
157
|
+
updatedMemory,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -20,7 +20,9 @@ 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 { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
|
|
23
24
|
import type { NarrativeStateStore } from "../../../storage/narrative/narrative-state-store.js";
|
|
25
|
+
import type { NarrativeTracePayload } from "../../../observability/services/lived-experience-audit.js";
|
|
24
26
|
export interface HeartbeatDecisionTracePayload {
|
|
25
27
|
scope: RuntimeScope;
|
|
26
28
|
status: HeartbeatCycleStatus;
|
|
@@ -46,7 +48,7 @@ export interface HeartbeatQuietWorkflowDeps {
|
|
|
46
48
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
47
49
|
* Exported for unit tests (CR-M1 wiring).
|
|
48
50
|
*/
|
|
49
|
-
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor">): Promise<HeartbeatCycleResult>;
|
|
51
|
+
export declare function resolveAllowedIntentResult(intent: CandidateIntent, runtime: HeartbeatRuntimeSnapshot, inputs: SnapshotInputs, signal: HeartbeatSignal, deps: Pick<HeartbeatDeps, "outreachDispatch" | "quietWorkflow" | "connectorExecutor" | "state" | "workspaceRoot">): Promise<HeartbeatCycleResult>;
|
|
50
52
|
export interface HeartbeatDeps {
|
|
51
53
|
/** Load snapshot inputs from state-system */
|
|
52
54
|
loadSnapshotInputs: () => Promise<SnapshotInputs>;
|
|
@@ -61,6 +63,14 @@ export interface HeartbeatDeps {
|
|
|
61
63
|
connectorExecutor?: ConnectorExecutor;
|
|
62
64
|
/** T2.1.5: when present, heartbeat writes a source-backed NarrativeState revision after each cycle. */
|
|
63
65
|
narrativeStateStore?: NarrativeStateStore;
|
|
66
|
+
/** T5.1.2: when present, heartbeat records a NarrativeTrace after successful narrative state update. */
|
|
67
|
+
recordNarrativeTrace?: (payload: NarrativeTracePayload) => Promise<void>;
|
|
68
|
+
/** T3.3.1: when present, successful connector effects write LifeEvidence artifacts. */
|
|
69
|
+
state?: StateDatabase;
|
|
70
|
+
/** T3.3.1: workspace root for evidence artifact paths. */
|
|
71
|
+
workspaceRoot?: string;
|
|
72
|
+
/** T2.4.1: when present, planner resolves platform-specific intents. */
|
|
73
|
+
connectorRegistry?: CapabilityContractRegistry;
|
|
64
74
|
}
|
|
65
75
|
/**
|
|
66
76
|
* Ingest a heartbeat rhythm signal and drive one full decision round.
|
|
@@ -8,6 +8,8 @@ import { buildJudgeOutreachInputFromSnapshot } from "../outreach/judge-input-fro
|
|
|
8
8
|
import { runSourceBackedQuiet } from "../quiet/run-source-backed-quiet.js";
|
|
9
9
|
import { toCapabilityIntent } from "../orchestrator/effect-dispatcher.js";
|
|
10
10
|
import { updateNarrativeAfterEffect } from "../orchestrator/narrative-update.js";
|
|
11
|
+
import { mapLifeEvidence } from "../../../connectors/base/map-life-evidence.js";
|
|
12
|
+
import { appendLifeEvidence } from "../../../storage/life-evidence/append-life-evidence.js";
|
|
11
13
|
/**
|
|
12
14
|
* Resolves the heartbeat outcome for a guard-allowed intent (outreach dispatch, quiet orchestration, or default).
|
|
13
15
|
* Exported for unit tests (CR-M1 wiring).
|
|
@@ -52,19 +54,53 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
52
54
|
intent.kind === "maintenance";
|
|
53
55
|
const connectorUnwired = intent.effectClass === "connector_action";
|
|
54
56
|
if (connectorUnwired && deps.connectorExecutor) {
|
|
57
|
+
if (!intent.platformId || intent.platformId === "unknown") {
|
|
58
|
+
return {
|
|
59
|
+
scope: "rhythm",
|
|
60
|
+
status: "intent_selected",
|
|
61
|
+
selectedIntentId: intent.id,
|
|
62
|
+
decisionId: `decision:${intent.id}:${Date.now()}`,
|
|
63
|
+
reasons: ["connector_dispatch_unavailable"],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const decisionId = `decision:${intent.id}:${Date.now()}`;
|
|
55
67
|
const result = await deps.connectorExecutor.executeEffect({
|
|
56
|
-
platformId: intent.platformId
|
|
68
|
+
platformId: intent.platformId,
|
|
57
69
|
intent: toCapabilityIntent(intent),
|
|
58
70
|
payload: {},
|
|
59
|
-
decisionId
|
|
71
|
+
decisionId,
|
|
60
72
|
intentId: intent.id,
|
|
61
73
|
idempotencyKey: `idem:${intent.id}:${Date.now()}`,
|
|
62
74
|
});
|
|
75
|
+
// T3.3.1: on success, map connector result to life evidence and append.
|
|
76
|
+
// On failure or empty result, no evidence is fabricated — attempt audit
|
|
77
|
+
// is already recorded by the connector policy layer telemetry.
|
|
78
|
+
if (result.status === "success" &&
|
|
79
|
+
deps.state &&
|
|
80
|
+
deps.workspaceRoot) {
|
|
81
|
+
try {
|
|
82
|
+
const candidate = mapLifeEvidence({
|
|
83
|
+
platformId: intent.platformId,
|
|
84
|
+
intent: toCapabilityIntent(intent),
|
|
85
|
+
result,
|
|
86
|
+
observedAt: new Date().toISOString(),
|
|
87
|
+
});
|
|
88
|
+
if (candidate) {
|
|
89
|
+
await appendLifeEvidence(deps.state, deps.workspaceRoot, candidate);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Evidence append must not break the heartbeat cycle.
|
|
94
|
+
// Missing evidence will be reflected in the next snapshot load.
|
|
95
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
96
|
+
console.warn(`[heartbeat] evidence append failed for ${intent.platformId ?? "unknown"}: ${errorMessage}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
63
99
|
const base = {
|
|
64
100
|
scope: "rhythm",
|
|
65
101
|
status: "intent_selected",
|
|
66
102
|
selectedIntentId: intent.id,
|
|
67
|
-
decisionId
|
|
103
|
+
decisionId,
|
|
68
104
|
reasons: result.status === "success"
|
|
69
105
|
? ["connector_effect_executed"]
|
|
70
106
|
: result.status === "retryable_failure"
|
|
@@ -91,7 +127,7 @@ export async function resolveAllowedIntentResult(intent, runtime, inputs, signal
|
|
|
91
127
|
* is never blocked by a store failure. Store failures are optionally traced
|
|
92
128
|
* via recordDecisionTrace so operators can monitor store health.
|
|
93
129
|
*/
|
|
94
|
-
async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store, recordTrace, signal) {
|
|
130
|
+
async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store, recordTrace, signal, recordNarrativeTrace) {
|
|
95
131
|
if (!store)
|
|
96
132
|
return;
|
|
97
133
|
try {
|
|
@@ -103,6 +139,33 @@ async function maybeUpdateNarrativeState(result, selectedIntent, runtime, store,
|
|
|
103
139
|
priorNarrative: prior,
|
|
104
140
|
});
|
|
105
141
|
await store.updateNarrativeState(update);
|
|
142
|
+
// T5.1.2: record NarrativeTrace on successful state update
|
|
143
|
+
if (recordNarrativeTrace) {
|
|
144
|
+
try {
|
|
145
|
+
await recordNarrativeTrace({
|
|
146
|
+
traceId: `narrative_trace:${crypto.randomUUID()}`,
|
|
147
|
+
narrativeId: update.narrativeId,
|
|
148
|
+
revision: update.revision,
|
|
149
|
+
updateSource: "heartbeat",
|
|
150
|
+
sourceRefs: update.sourceRefs.map((r) => ({
|
|
151
|
+
id: r.sourceId,
|
|
152
|
+
kind: r.kind,
|
|
153
|
+
uri: r.url,
|
|
154
|
+
})),
|
|
155
|
+
unsupportedClaims: update.unsupportedClaims,
|
|
156
|
+
groundingStatus: update.unsupportedClaims.length > 0
|
|
157
|
+
? "degraded"
|
|
158
|
+
: update.status === "insufficient_sources"
|
|
159
|
+
? "blocked"
|
|
160
|
+
: "pass",
|
|
161
|
+
goalInfluenceRefs: selectedIntent?.goalInfluenceRefs ?? [],
|
|
162
|
+
createdAt: update.updatedAt,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// trace emission must not block the cycle
|
|
167
|
+
}
|
|
168
|
+
}
|
|
106
169
|
}
|
|
107
170
|
catch {
|
|
108
171
|
// degrade silently; narrative update is best-effort
|
|
@@ -136,7 +199,12 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
136
199
|
const snapshot = buildContinuitySnapshot(inputs);
|
|
137
200
|
const timestamp = signal.payload.timestamp;
|
|
138
201
|
const runtime = buildHeartbeatRuntimeSnapshot(timestamp, inputs, snapshot);
|
|
139
|
-
const rawCandidates = planCandidateIntents(runtime
|
|
202
|
+
const rawCandidates = planCandidateIntents(runtime, {
|
|
203
|
+
acceptedGoals: inputs.acceptedGoals,
|
|
204
|
+
connectorRegistry: deps.connectorRegistry,
|
|
205
|
+
narrativeState: runtime.narrativeState,
|
|
206
|
+
relationshipMemory: runtime.relationshipMemory,
|
|
207
|
+
});
|
|
140
208
|
const { candidates } = applyGoalPriority(rawCandidates, inputs.acceptedGoals);
|
|
141
209
|
const emitTrace = async (result) => {
|
|
142
210
|
if (!deps.recordDecisionTrace)
|
|
@@ -178,7 +246,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
178
246
|
? { ...resolved, reasons: evaluation.reasons }
|
|
179
247
|
: resolved;
|
|
180
248
|
await emitTrace(result);
|
|
181
|
-
await maybeUpdateNarrativeState(result, intent, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
249
|
+
await maybeUpdateNarrativeState(result, intent, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
182
250
|
return result;
|
|
183
251
|
}
|
|
184
252
|
if (evaluation.verdict === "defer") {
|
|
@@ -196,7 +264,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
196
264
|
reasons: ["silent_no_candidates"],
|
|
197
265
|
};
|
|
198
266
|
await emitTrace(result);
|
|
199
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
267
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
200
268
|
return result;
|
|
201
269
|
}
|
|
202
270
|
if (!anyAllow && anyDefer && !anyDeny) {
|
|
@@ -206,7 +274,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
206
274
|
reasons: denyReasons.length > 0 ? denyReasons : ["all_candidates_deferred"],
|
|
207
275
|
};
|
|
208
276
|
await emitTrace(result);
|
|
209
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
277
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
210
278
|
return result;
|
|
211
279
|
}
|
|
212
280
|
if (!anyAllow && denyReasons.length > 0) {
|
|
@@ -216,7 +284,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
216
284
|
reasons: denyReasons,
|
|
217
285
|
};
|
|
218
286
|
await emitTrace(result);
|
|
219
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
287
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
220
288
|
return result;
|
|
221
289
|
}
|
|
222
290
|
const result = {
|
|
@@ -225,7 +293,7 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
225
293
|
reasons: ["no_allow_verdict"],
|
|
226
294
|
};
|
|
227
295
|
await emitTrace(result);
|
|
228
|
-
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal);
|
|
296
|
+
await maybeUpdateNarrativeState(result, undefined, runtime, deps.narrativeStateStore, deps.recordDecisionTrace, signal, deps.recordNarrativeTrace);
|
|
229
297
|
return result;
|
|
230
298
|
}
|
|
231
299
|
/**
|
|
@@ -21,6 +21,8 @@ export interface HeartbeatRuntimeSnapshot {
|
|
|
21
21
|
lifeEvidence: PlannerLifeEvidenceSlice;
|
|
22
22
|
rhythmWindow: PlannerRhythmWindowSlice;
|
|
23
23
|
hardGuards: HardGuardDeps;
|
|
24
|
+
narrativeState?: import("../../../storage/narrative/narrative-state-store.js").NarrativeState;
|
|
25
|
+
relationshipMemory?: import("../../../storage/relationship/relationship-memory-store.js").RelationshipMemory;
|
|
24
26
|
}
|
|
25
27
|
export declare function buildLifeEvidenceSliceFromInputs(inputs: SnapshotInputs): PlannerLifeEvidenceSlice;
|
|
26
28
|
export declare function buildHardGuardDeps(continuity: ContinuitySnapshot, inputs: SnapshotInputs): HardGuardDeps;
|
|
@@ -31,5 +31,5 @@ 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 { continuity, lifeEvidence, rhythmWindow, hardGuards };
|
|
34
|
+
return { continuity, lifeEvidence, rhythmWindow, hardGuards, narrativeState: inputs.narrativeState, relationshipMemory: inputs.relationshipMemory };
|
|
35
35
|
}
|
|
@@ -10,7 +10,8 @@ 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 {
|
|
13
|
+
import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
|
|
14
|
+
import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
|
|
14
15
|
export interface SnapshotInputs {
|
|
15
16
|
mode: TopLevelMode;
|
|
16
17
|
currentWindowId: string;
|
|
@@ -43,7 +44,20 @@ export interface SnapshotInputs {
|
|
|
43
44
|
/** When present, outreach judgment uses this user-interest read model (T4.2.2). */
|
|
44
45
|
userInterestSnapshot?: UserInterestSnapshot;
|
|
45
46
|
/** T2.1.4: accepted goals to influence candidate intent priority. */
|
|
46
|
-
acceptedGoals?:
|
|
47
|
+
acceptedGoals?: Array<{
|
|
48
|
+
goalId: string;
|
|
49
|
+
description: string;
|
|
50
|
+
completionCriteria?: string;
|
|
51
|
+
status: "proposal" | "accepted" | "rejected" | "completed" | "paused";
|
|
52
|
+
origin: "owner_set" | "agent_proposed" | "policy_seeded";
|
|
53
|
+
acceptedBy?: "owner" | "policy_allowlist";
|
|
54
|
+
}>;
|
|
55
|
+
/** When present, signals that acceptedGoals load failed (distinguishes from empty). */
|
|
56
|
+
acceptedGoalsLoadError?: string;
|
|
57
|
+
/** When present, planner uses narrative focus to influence candidate priority. */
|
|
58
|
+
narrativeState?: NarrativeState;
|
|
59
|
+
/** When present, planner uses relationship memory to influence outreach timing. */
|
|
60
|
+
relationshipMemory?: RelationshipMemory;
|
|
47
61
|
}
|
|
48
62
|
/**
|
|
49
63
|
* Build a ContinuitySnapshot from loaded inputs.
|
|
@@ -3,11 +3,24 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `applyGoalPriority` adjusts candidate intent priorities based on accepted AgentGoals.
|
|
5
5
|
* Priority order: user_task > accepted_goal > rhythm.
|
|
6
|
-
* Only goals with status === "accepted"
|
|
6
|
+
* Only goals with status === "accepted" are considered.
|
|
7
|
+
* Agent-proposed goals are included ONLY if policy-accepted (acceptedBy === "policy_allowlist").
|
|
7
8
|
* All other statuses (proposal / rejected / completed / paused) are implicitly excluded.
|
|
8
9
|
*/
|
|
9
10
|
import type { CandidateIntent } from "../types.js";
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Minimal goal context used by the priority module to avoid coupling
|
|
13
|
+
* to the full AgentGoal schema. M-03 decoupling.
|
|
14
|
+
*/
|
|
15
|
+
export interface GoalPriorityContext {
|
|
16
|
+
goalId: string;
|
|
17
|
+
description: string;
|
|
18
|
+
completionCriteria?: string;
|
|
19
|
+
status: "proposal" | "accepted" | "rejected" | "completed" | "paused";
|
|
20
|
+
origin: "owner_set" | "agent_proposed" | "policy_seeded";
|
|
21
|
+
acceptedBy?: "owner" | "policy_allowlist";
|
|
22
|
+
}
|
|
23
|
+
export declare function isGoalRelatedToCandidate(goal: GoalPriorityContext, candidate: CandidateIntent): boolean;
|
|
11
24
|
export interface ApplyGoalPriorityResult {
|
|
12
25
|
candidates: CandidateIntent[];
|
|
13
26
|
goalInfluences: Array<{
|
|
@@ -16,4 +29,4 @@ export interface ApplyGoalPriorityResult {
|
|
|
16
29
|
boost: number;
|
|
17
30
|
}>;
|
|
18
31
|
}
|
|
19
|
-
export declare function applyGoalPriority(candidates: CandidateIntent[], goals:
|
|
32
|
+
export declare function applyGoalPriority(candidates: CandidateIntent[], goals: GoalPriorityContext[] | undefined): ApplyGoalPriorityResult;
|
|
@@ -6,15 +6,15 @@
|
|
|
6
6
|
* range from 40–100, so 200 provides ample headroom without overflow.
|
|
7
7
|
*/
|
|
8
8
|
const GOAL_PRIORITY_BOOST = 20;
|
|
9
|
-
function isGoalRelatedToCandidate(goal, candidate) {
|
|
10
|
-
|
|
11
|
-
return false;
|
|
12
|
-
const goalText = `${goal.description} ${goal.completionCriteria}`.toLowerCase();
|
|
13
|
-
const platformId = candidate.platformId.toLowerCase();
|
|
9
|
+
export function isGoalRelatedToCandidate(goal, candidate) {
|
|
10
|
+
const goalText = `${goal.description} ${goal.completionCriteria ?? ""}`.toLowerCase();
|
|
14
11
|
// Direct platformId mention in goal text
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
if (candidate.platformId) {
|
|
13
|
+
const platformId = candidate.platformId.toLowerCase();
|
|
14
|
+
if (goalText.includes(platformId))
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
// Fallback: Goal description contains candidate summary keywords
|
|
18
18
|
const summaryWords = candidate.summary.toLowerCase().split(/\s+/);
|
|
19
19
|
for (const word of summaryWords) {
|
|
20
20
|
if (word.length > 3 && goalText.includes(word))
|
|
@@ -23,7 +23,8 @@ function isGoalRelatedToCandidate(goal, candidate) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
export function applyGoalPriority(candidates, goals) {
|
|
26
|
-
const acceptedGoals = (goals ?? []).filter((g) => g.status === "accepted" &&
|
|
26
|
+
const acceptedGoals = (goals ?? []).filter((g) => g.status === "accepted" &&
|
|
27
|
+
(g.origin !== "agent_proposed" || g.acceptedBy === "policy_allowlist"));
|
|
27
28
|
if (acceptedGoals.length === 0) {
|
|
28
29
|
return {
|
|
29
30
|
candidates: candidates.map((c) => ({
|
|
@@ -4,10 +4,38 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { CandidateIntent, ContinuitySnapshot, DecisionBasis } from "../types.js";
|
|
6
6
|
import type { HeartbeatRuntimeSnapshot } from "../heartbeat/runtime-snapshot.js";
|
|
7
|
+
import type { CapabilityContractRegistry } from "../../../connectors/base/manifest.js";
|
|
8
|
+
import type { NarrativeState } from "../../../storage/narrative/narrative-state-store.js";
|
|
9
|
+
import type { RelationshipMemory } from "../../../storage/relationship/relationship-memory-store.js";
|
|
10
|
+
import { type PlatformResolutionContext } from "./platform-capability-router.js";
|
|
11
|
+
import { type GoalPriorityContext } from "./goal-priority.js";
|
|
12
|
+
/** Alias for GoalPriorityContext to keep intent-planner local naming consistent. M-03 decoupling. */
|
|
13
|
+
export type GoalContext = GoalPriorityContext;
|
|
14
|
+
export interface PlanIntentOptions {
|
|
15
|
+
narrativeState?: NarrativeState;
|
|
16
|
+
relationshipMemory?: RelationshipMemory;
|
|
17
|
+
budgetCheck?: boolean;
|
|
18
|
+
multiSource?: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Factory for planning a candidate intent of a given kind.
|
|
22
|
+
* M-04: consolidates the previously separate plan{Work,Exploration,Social,Outreach}Intents.
|
|
23
|
+
*/
|
|
24
|
+
export declare function planIntentWithKind(kind: "work" | "exploration" | "social" | "outreach", basePriority: number, runtime: HeartbeatRuntimeSnapshot, context: PlatformResolutionContext, registry?: CapabilityContractRegistry, options?: PlanIntentOptions): CandidateIntent[];
|
|
25
|
+
export interface PlanCandidateIntentsOptions {
|
|
26
|
+
/** T2.4.1: accepted goals for platform-specific resolution. */
|
|
27
|
+
acceptedGoals?: GoalContext[];
|
|
28
|
+
/** T2.4.1: optional connector registry for capability validation. */
|
|
29
|
+
connectorRegistry?: CapabilityContractRegistry;
|
|
30
|
+
/** CR-02: optional narrative state to influence candidate priority. */
|
|
31
|
+
narrativeState?: NarrativeState;
|
|
32
|
+
/** CR-02: optional relationship memory to influence outreach timing. */
|
|
33
|
+
relationshipMemory?: RelationshipMemory;
|
|
34
|
+
}
|
|
7
35
|
/**
|
|
8
36
|
* Plan ordered candidates for one heartbeat turn using rhythm window + life evidence slice.
|
|
9
37
|
*/
|
|
10
|
-
export declare function planCandidateIntents(runtime: HeartbeatRuntimeSnapshot): CandidateIntent[];
|
|
38
|
+
export declare function planCandidateIntents(runtime: HeartbeatRuntimeSnapshot, options?: PlanCandidateIntentsOptions): CandidateIntent[];
|
|
11
39
|
/** @deprecated Continuity-only helper for tests; prefer `planCandidateIntents` + `buildHeartbeatRuntimeSnapshot`. */
|
|
12
40
|
export declare function planIntent(snapshot: ContinuitySnapshot): CandidateIntent[];
|
|
13
41
|
export declare function decideDecisionBasis(intent: CandidateIntent): DecisionBasis;
|