@haaaiawd/second-nature 0.1.25 → 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 +1 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/runtime/cli/commands/goal.d.ts +2 -0
- package/runtime/cli/commands/goal.js +5 -1
- package/runtime/cli/commands/index.js +1 -1
- package/runtime/cli/explain/resolve-subject.js +3 -0
- package/runtime/cli/ops/ops-router.js +13 -5
- 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.js +81 -10
- package/runtime/cli/read-models/types.d.ts +10 -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 +8 -1
- package/runtime/core/second-nature/heartbeat/heartbeat-loop.js +45 -4
- 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 +14 -2
- package/runtime/core/second-nature/orchestrator/goal-priority.js +2 -2
- 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/observability/query/explain-query.d.ts +3 -0
- package/runtime/observability/query/explain-query.js +9 -0
- package/runtime/shared/types/credential.d.ts +1 -1
- package/runtime/storage/chronicle/session-chronicle-store.d.ts +1 -1
- package/runtime/storage/services/credential-vault.d.ts +18 -0
- package/runtime/storage/services/credential-vault.js +73 -3
|
@@ -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"
|
|
@@ -163,7 +199,12 @@ export async function ingestRhythmSignal(signal, deps) {
|
|
|
163
199
|
const snapshot = buildContinuitySnapshot(inputs);
|
|
164
200
|
const timestamp = signal.payload.timestamp;
|
|
165
201
|
const runtime = buildHeartbeatRuntimeSnapshot(timestamp, inputs, snapshot);
|
|
166
|
-
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
|
+
});
|
|
167
208
|
const { candidates } = applyGoalPriority(rawCandidates, inputs.acceptedGoals);
|
|
168
209
|
const emitTrace = async (result) => {
|
|
169
210
|
if (!deps.recordDecisionTrace)
|
|
@@ -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.
|
|
@@ -8,7 +8,19 @@
|
|
|
8
8
|
* All other statuses (proposal / rejected / completed / paused) are implicitly excluded.
|
|
9
9
|
*/
|
|
10
10
|
import type { CandidateIntent } from "../types.js";
|
|
11
|
-
|
|
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;
|
|
12
24
|
export interface ApplyGoalPriorityResult {
|
|
13
25
|
candidates: CandidateIntent[];
|
|
14
26
|
goalInfluences: Array<{
|
|
@@ -17,4 +29,4 @@ export interface ApplyGoalPriorityResult {
|
|
|
17
29
|
boost: number;
|
|
18
30
|
}>;
|
|
19
31
|
}
|
|
20
|
-
export declare function applyGoalPriority(candidates: CandidateIntent[], goals:
|
|
32
|
+
export declare function applyGoalPriority(candidates: CandidateIntent[], goals: GoalPriorityContext[] | undefined): ApplyGoalPriorityResult;
|
|
@@ -6,8 +6,8 @@
|
|
|
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
|
-
const goalText = `${goal.description} ${goal.completionCriteria}`.toLowerCase();
|
|
9
|
+
export function isGoalRelatedToCandidate(goal, candidate) {
|
|
10
|
+
const goalText = `${goal.description} ${goal.completionCriteria ?? ""}`.toLowerCase();
|
|
11
11
|
// Direct platformId mention in goal text
|
|
12
12
|
if (candidate.platformId) {
|
|
13
13
|
const platformId = candidate.platformId.toLowerCase();
|
|
@@ -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;
|
|
@@ -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 {};
|