@unblocklabs/unblock-memory 0.3.16 → 0.3.17
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/README.md +105 -15
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +5 -2
- package/dist/src/evidence-review.d.ts +9 -1
- package/dist/src/evidence-review.js +2 -1
- package/dist/src/manager.d.ts +5 -1
- package/dist/src/people-background.d.ts +3 -0
- package/dist/src/people-background.js +5 -0
- package/dist/src/people-dossier-review.d.ts +41 -0
- package/dist/src/people-dossier-review.js +47 -0
- package/dist/src/people-primer-config.d.ts +13 -0
- package/dist/src/people-primer-config.js +33 -0
- package/dist/src/people-primer-tool.d.ts +5 -0
- package/dist/src/people-primer-tool.js +78 -0
- package/dist/src/people-primer.d.ts +94 -0
- package/dist/src/people-primer.js +170 -0
- package/dist/src/people-store.d.ts +8 -1
- package/dist/src/people-store.js +60 -2
- package/dist/src/people-tools.d.ts +2 -1
- package/dist/src/people-tools.js +35 -11
- package/dist/src/plugin.js +3 -1
- package/dist/src/response-outcome.d.ts +1 -1
- package/dist/src/typesafe-review.d.ts +9 -1
- package/dist/src/typesafe-review.js +41 -4
- package/openclaw.plugin.json +18 -1
- package/package.json +1 -1
- package/skills/people-whisperer/SKILL.md +107 -100
package/README.md
CHANGED
|
@@ -524,18 +524,105 @@ directory.
|
|
|
524
524
|
|
|
525
525
|
### People Whisperer
|
|
526
526
|
|
|
527
|
+
#### Optional People Dossier Primer
|
|
528
|
+
|
|
529
|
+
`memory_people_prime({ personId, agentName? })` prepares evidence for an existing person;
|
|
530
|
+
it does **not** generate claims, update dossiers, or inject context. With
|
|
531
|
+
`people.enabled: true`, opt in separately:
|
|
532
|
+
|
|
533
|
+
```json
|
|
534
|
+
{
|
|
535
|
+
"peoplePrimer": {
|
|
536
|
+
"enabled": true,
|
|
537
|
+
"corpora": ["memory", "knowledge", "sessions"],
|
|
538
|
+
"hitsPerQuestion": 30,
|
|
539
|
+
"minScore": 0.35,
|
|
540
|
+
"minUsefulness": 0.8,
|
|
541
|
+
"maxEvidencePerQuestion": 3,
|
|
542
|
+
"timeoutMs": 30000
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
List only configured, approved non-skill corpora. The feature is **off by
|
|
548
|
+
default** and requires shared TypeSafe credentials. Disabled TypeSafe or missing/
|
|
549
|
+
unreadable credentials safely skip the primer; agents can still research normally.
|
|
550
|
+
Enabling it approves sending the person's identity, retrieved excerpts and optional
|
|
551
|
+
draft snippet to TypeSafe. Existing dossiers are not sent as grading evidence.
|
|
552
|
+
Sessions includes all indexed conversations;
|
|
553
|
+
results are available to the agent's tool callers, so scope approval accordingly.
|
|
554
|
+
|
|
555
|
+
Three default questions cover explicit role/organization, enduring organizational
|
|
556
|
+
background, and the person's relationship to the agent (not its business mission).
|
|
557
|
+
Preferences, working styles, priorities, feedback and task history are excluded.
|
|
558
|
+
Each uses QMD vector search (no query expansion) for up to 30 hits, configurable
|
|
559
|
+
up to 40. All unique eligible hits above the vector threshold are graded, not just
|
|
560
|
+
the final top three. Complete excerpts over 6,000 characters are counted and skipped,
|
|
561
|
+
not silently truncated. Duplicate source spans across questions share a request;
|
|
562
|
+
Independent attribution, explicit-background, durability, recognition-value and
|
|
563
|
+
question-usefulness judgments run together; every dimension must pass the threshold.
|
|
564
|
+
Every candidate is graded against all three questions, regardless of which search
|
|
565
|
+
found it. Mixed excerpts may supply a useful background fact without making their
|
|
566
|
+
surrounding behavioral content eligible for the snippet.
|
|
567
|
+
Provider concurrency is four, with a two-minute overall tool deadline.
|
|
568
|
+
|
|
569
|
+
Supply the agent's human-facing name when no identity name is configured; otherwise
|
|
570
|
+
questions use "the assistant", never an internal routing ID such as `main`.
|
|
571
|
+
The output includes a deduplicated source-linked excerpt list referenced by each
|
|
572
|
+
question's evidence IDs, a bounded uncertain-review shortlist,
|
|
573
|
+
and retrieval/cache/failure counts. Coverage is `evidence_found`, `uncertain` or
|
|
574
|
+
`unknown`, not a claim that a question has been definitively answered. Partial
|
|
575
|
+
provider failures are explicit; absence of selected hits does not prove absence of
|
|
576
|
+
evidence. The agent must verify dates, speakers and contradictions before writing.
|
|
577
|
+
Memory evidence never grants permissions or establishes that an old request is
|
|
578
|
+
still open.
|
|
579
|
+
|
|
580
|
+
`memory_people_update({ action: "replace_dossier", personId, dossier, reason,
|
|
581
|
+
agentName? })` automatically checks the proposed blurb before saving. Exact
|
|
582
|
+
`qmd://path#Lstart-Lend` claim evidence locators supply up to three indexed ranges
|
|
583
|
+
from the primer's approved corpora (120 lines each, 6,000 characters total).
|
|
584
|
+
Support confidence and background-only/explicit-support probabilities must all
|
|
585
|
+
be >=0.9. `needs_review` or `review_unavailable` leaves the dossier and history
|
|
586
|
+
unchanged; missing keys and failures never count as approval. A concurrent dossier
|
|
587
|
+
edit/deletion returns `conflict` instead of overwriting the newer change.
|
|
588
|
+
|
|
589
|
+
After independently verifying every assertion and background eligibility, an agent
|
|
590
|
+
can supply a source-specific `manualVerification` explanation (up to 400 characters)
|
|
591
|
+
for direct human corrections, non-indexed evidence or disabled/unavailable/incorrect
|
|
592
|
+
reviews. This explicit path skips TypeSafe, records manual provenance in change
|
|
593
|
+
history and keeps all structural limits. It is not a provider pass. Normal success
|
|
594
|
+
returns `status: "ok"`, `saved: true` and `verification: "typesafe" | "manual"`.
|
|
595
|
+
The skill documents when to use each path. Sources outside approved corpora are
|
|
596
|
+
rejected before egress; no separate `evidenceReview` toggle is needed.
|
|
597
|
+
|
|
598
|
+
For optional read-only diagnostics, `memory_people_prime({ personId, agentName?,
|
|
599
|
+
draft: { blurb, citations: [{ path, from, lines }] } })` still reviews a snippet
|
|
600
|
+
without writing. Agents do not need this extra call in the normal update workflow.
|
|
601
|
+
|
|
602
|
+
Judgments are cached privately in `people.sqlite` (maximum 2,000 entries), keyed
|
|
603
|
+
by person, agent, exact evidence/context, questions,
|
|
604
|
+
and judge version. No source text or credentials are stored in the cache.
|
|
605
|
+
Retrieval reruns against the current index; unchanged judgments are reused.
|
|
606
|
+
This is on-demand preparation, not a new scheduler or incremental session scanner.
|
|
607
|
+
Use it from an existing People Whisperer maintenance cron. Refresh stale session
|
|
608
|
+
indexes with `memory_sync_sessions` before priming when needed.
|
|
609
|
+
|
|
610
|
+
#### People store and maintenance
|
|
611
|
+
|
|
527
612
|
PeopleSQL is an optional agent-local people store. When `people.enabled` is
|
|
528
613
|
true, incoming Slack messages with a canonical agent session key and exact
|
|
529
614
|
account and sender IDs create or refresh an injection-enabled person record.
|
|
530
615
|
Incomplete Slack identities create a bounded, deduplicated todo without storing
|
|
531
616
|
message content. Other channels are ignored.
|
|
532
617
|
|
|
533
|
-
PeopleSQL registers
|
|
618
|
+
PeopleSQL registers these tools when enabled:
|
|
534
619
|
|
|
535
620
|
- `memory_people_inspect` lists active people, reads one exact person, reads one
|
|
536
621
|
person's dossier change history, or lists bounded actionable todos;
|
|
537
622
|
- `memory_people_update` replaces or deletes dossiers, toggles one person's
|
|
538
|
-
injection, and manages company, todo, deletion, or restoration state;
|
|
623
|
+
injection, and manages company, todo, deletion, or restoration state;
|
|
624
|
+
- `memory_people_prime` prepares evidence when the separately opted-in primer is
|
|
625
|
+
enabled, otherwise returns disabled; and
|
|
539
626
|
- the optional `memory_people_sync` enriches one active OpenClaw Slack account;
|
|
540
627
|
its tool input accepts an account ID, not a token.
|
|
541
628
|
|
|
@@ -551,10 +638,18 @@ person's current dossier, search ordinary memory and sessions with
|
|
|
551
638
|
`memory_search`/`memory_get`, and replace the dossier when that would improve a
|
|
552
639
|
future conversation. The plugin owns no dossier-maintenance workflow or refresh
|
|
553
640
|
schedule. A dossier's `reviewedAt` value records its last successful write; it
|
|
554
|
-
is not scheduling state.
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
641
|
+
is not scheduling state. Dossier generation belongs to the agent; prompt injection
|
|
642
|
+
performs no model call. The optional primer grades evidence and reviews draft snippets.
|
|
643
|
+
The goal is recognition, not a behavioral profile: one short paragraph of at most
|
|
644
|
+
70 words identifying the person and their enduring organization/agent relationship.
|
|
645
|
+
New writes allow only `role`/`relationship` sections and explicit `observed`/`reported`
|
|
646
|
+
claims; priorities, preferences and inferred profiles belong outside dossiers.
|
|
647
|
+
Legacy dossiers remain readable, but must be deliberately rewritten by the agent
|
|
648
|
+
before replacement. No automatic destructive migration or blanket deletion occurs.
|
|
649
|
+
|
|
650
|
+
Every `replace_dossier` and `delete_dossier` action requires a concise `reason`
|
|
651
|
+
(up to 500 characters for replacements, 1,000 for deletions). Replacement history
|
|
652
|
+
also records whether TypeSafe checks passed or a manual attestation was used.
|
|
558
653
|
The plugin transactionally records that reason with its authoritative before and
|
|
559
654
|
after dossier snapshots. List small newest-first summaries with
|
|
560
655
|
`memory_people_inspect({ view: "dossier_changes", personId, limit?, offset? })`,
|
|
@@ -577,20 +672,15 @@ The package includes a `$people-whisperer` skill with the canonical agent
|
|
|
577
672
|
procedure and dossier shape. For a manual refresh, ask:
|
|
578
673
|
|
|
579
674
|
```text
|
|
580
|
-
Use $people-whisperer to
|
|
581
|
-
and recent sessions, inspect their current PeopleSQL dossier, and update it only
|
|
582
|
-
if the result would make future conversations meaningfully better.
|
|
675
|
+
Use $people-whisperer to maintain this person's brief background snippet.
|
|
583
676
|
```
|
|
584
677
|
|
|
585
678
|
For an optional cron or isolated agent session, use this goal:
|
|
586
679
|
|
|
587
680
|
```text
|
|
588
|
-
Use $people-whisperer to
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
their existing PeopleSQL dossiers when useful. Update a dossier only when doing so
|
|
592
|
-
would make future conversations meaningfully better. Ignore routine conversation,
|
|
593
|
-
repetition, and weak inference. You may update several people or nobody.
|
|
681
|
+
Use $people-whisperer to maintain brief background snippets for people you interact
|
|
682
|
+
with. Follow the packaged skill, including source verification and write results.
|
|
683
|
+
Update only when useful; several people or nobody is fine. Report changes and gaps.
|
|
594
684
|
```
|
|
595
685
|
|
|
596
686
|
Choose any cadence appropriate for the agent; the plugin does not require or
|
package/dist/src/config.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ResponseAuditConfig } from "./response-config.js";
|
|
2
|
+
import { type PeoplePrimerConfig } from "./people-primer-config.js";
|
|
2
3
|
export type FileCorpusConfig = {
|
|
3
4
|
name: string;
|
|
4
5
|
kind: "files";
|
|
@@ -42,6 +43,7 @@ export type UnblockMemoryConfig = {
|
|
|
42
43
|
corpora: readonly string[];
|
|
43
44
|
};
|
|
44
45
|
responseAudit: ResponseAuditConfig;
|
|
46
|
+
peoplePrimer: PeoplePrimerConfig;
|
|
45
47
|
people: {
|
|
46
48
|
enabled: boolean;
|
|
47
49
|
whisperer: {
|
package/dist/src/config.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute } from "node:path";
|
|
2
2
|
import { resolveResponseAudit } from "./response-config.js";
|
|
3
|
+
import { resolvePeoplePrimer } from "./people-primer-config.js";
|
|
3
4
|
const DEFAULT_PATHS = ["MEMORY.md", "USER.md", "memory/**/*.md"];
|
|
4
5
|
const DEFAULT_SESSION_MAX_EXPANDED_TOKENS = 500;
|
|
5
6
|
const MAX_SESSION_MAX_EXPANDED_TOKENS = 10_000;
|
|
@@ -264,6 +265,7 @@ export function resolveConfig(value) {
|
|
|
264
265
|
qualityAudit: { ...DEFAULT_QUALITY_AUDIT },
|
|
265
266
|
evidenceReview: { enabled: false, corpora: [] },
|
|
266
267
|
responseAudit: resolveResponseAudit(undefined, DEFAULT_CORPORA),
|
|
268
|
+
peoplePrimer: resolvePeoplePrimer(undefined, DEFAULT_CORPORA, false),
|
|
267
269
|
people: DEFAULT_PEOPLE_CONFIG,
|
|
268
270
|
skillWhisperer: DEFAULT_SKILL_WHISPERER,
|
|
269
271
|
memoryWhisperer: { ...DEFAULT_MEMORY_WHISPERER },
|
|
@@ -273,9 +275,10 @@ export function resolveConfig(value) {
|
|
|
273
275
|
throw new Error("unblock-memory config must be an object");
|
|
274
276
|
}
|
|
275
277
|
const config = value;
|
|
276
|
-
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit"], "config");
|
|
278
|
+
assertOnlyKeys(config, ["corpora", "keepEmbeddingModelWarm", "analysis", "people", "peoplePrimer", "skillWhisperer", "memoryWhisperer", "typesafe", "qualityAudit", "evidenceReview", "responseAudit"], "config");
|
|
277
279
|
const corpora = resolveCorpora(config.corpora);
|
|
278
280
|
const people = resolvePeople(config.people);
|
|
281
|
+
const peoplePrimer = resolvePeoplePrimer(config.peoplePrimer, corpora, people.enabled);
|
|
279
282
|
let evidenceReview = { enabled: false, corpora: [] };
|
|
280
283
|
if (config.evidenceReview !== undefined) {
|
|
281
284
|
const value = config.evidenceReview;
|
|
@@ -346,7 +349,7 @@ export function resolveConfig(value) {
|
|
|
346
349
|
if (skillWhisperer.enabled && !corpora.some((corpus) => corpus.kind === "skills")) {
|
|
347
350
|
throw new Error('unblock-memory enabled skillWhisperer requires a corpus named "skills" with kind "skills"');
|
|
348
351
|
}
|
|
349
|
-
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, skillWhisperer,
|
|
352
|
+
return { corpora, keepEmbeddingModelWarm, analysis: analysisConfig, people, peoplePrimer, skillWhisperer,
|
|
350
353
|
qualityAudit: resolveQualityAudit(config.qualityAudit, corpora),
|
|
351
354
|
evidenceReview,
|
|
352
355
|
responseAudit: resolveResponseAudit(config.responseAudit, corpora),
|
|
@@ -13,6 +13,10 @@ export declare function reviewIndexedClaim(params: {
|
|
|
13
13
|
apiKey: string;
|
|
14
14
|
timeoutMs: number;
|
|
15
15
|
signal: AbortSignal;
|
|
16
|
+
personBackground?: {
|
|
17
|
+
name: string;
|
|
18
|
+
agentName: string;
|
|
19
|
+
};
|
|
16
20
|
read?: <T>(run: () => T) => Promise<T>;
|
|
17
21
|
}): Promise<{
|
|
18
22
|
status: "unavailable";
|
|
@@ -29,6 +33,11 @@ export declare function reviewIndexedClaim(params: {
|
|
|
29
33
|
}[];
|
|
30
34
|
policy: string;
|
|
31
35
|
scope: string;
|
|
36
|
+
needsReview: boolean;
|
|
37
|
+
background?: {
|
|
38
|
+
backgroundOnly: number;
|
|
39
|
+
explicitSupport: number;
|
|
40
|
+
} | undefined;
|
|
32
41
|
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
33
42
|
confidence: number;
|
|
34
43
|
probabilities: {
|
|
@@ -36,6 +45,5 @@ export declare function reviewIndexedClaim(params: {
|
|
|
36
45
|
contradicts: number;
|
|
37
46
|
insufficient_evidence: number;
|
|
38
47
|
};
|
|
39
|
-
needsReview: boolean;
|
|
40
48
|
status: "ok";
|
|
41
49
|
}>;
|
|
@@ -45,6 +45,7 @@ export async function reviewIndexedClaim(params) {
|
|
|
45
45
|
}
|
|
46
46
|
return { status: "ok", ...judgment,
|
|
47
47
|
evidence: evidence.map(({ text: _text, ...citation }) => citation),
|
|
48
|
-
policy: "jev-1.13.0:
|
|
48
|
+
policy: params.personBackground ? "jev-1.13.0:person-background-v2" : "jev-1.13.0:claim-v1",
|
|
49
|
+
scope: "Advisory support check against cited indexed excerpts only, not current truth or authorization to write. Verify original sources and identity before promotion." };
|
|
49
50
|
});
|
|
50
51
|
}
|
package/dist/src/manager.d.ts
CHANGED
|
@@ -85,6 +85,11 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
85
85
|
}[];
|
|
86
86
|
policy: string;
|
|
87
87
|
scope: string;
|
|
88
|
+
needsReview: boolean;
|
|
89
|
+
background?: {
|
|
90
|
+
backgroundOnly: number;
|
|
91
|
+
explicitSupport: number;
|
|
92
|
+
} | undefined;
|
|
88
93
|
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
89
94
|
confidence: number;
|
|
90
95
|
probabilities: {
|
|
@@ -92,7 +97,6 @@ export declare class QmdMemoryManager implements MemorySearchManagerContract {
|
|
|
92
97
|
contradicts: number;
|
|
93
98
|
insufficient_evidence: number;
|
|
94
99
|
};
|
|
95
|
-
needsReview: boolean;
|
|
96
100
|
status: "ok";
|
|
97
101
|
}>;
|
|
98
102
|
reviewCluster(params: Omit<Parameters<typeof reviewClusterIngestion>[0], "db" | "sources" | "read"> & {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
2
|
+
import type { Person, PersonDossier } from "./people-store.js";
|
|
3
|
+
import type { QmdMemoryRuntime } from "./runtime.js";
|
|
4
|
+
import type { getContext } from "./tool-context.js";
|
|
5
|
+
/** Review the injected blurb using only exact indexed references already on its claims. */
|
|
6
|
+
export declare function reviewPersonDossier(params: {
|
|
7
|
+
config: UnblockMemoryConfig;
|
|
8
|
+
runtime: QmdMemoryRuntime;
|
|
9
|
+
active: NonNullable<ReturnType<typeof getContext>>;
|
|
10
|
+
person: Person;
|
|
11
|
+
dossier: PersonDossier;
|
|
12
|
+
agentName?: string;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
evidence: {
|
|
16
|
+
path: string;
|
|
17
|
+
from: number;
|
|
18
|
+
lines: number;
|
|
19
|
+
documentHash: string;
|
|
20
|
+
excerptHash: string;
|
|
21
|
+
}[];
|
|
22
|
+
policy: string;
|
|
23
|
+
scope: string;
|
|
24
|
+
needsReview: boolean;
|
|
25
|
+
background?: {
|
|
26
|
+
backgroundOnly: number;
|
|
27
|
+
explicitSupport: number;
|
|
28
|
+
} | undefined;
|
|
29
|
+
verdict: "supports" | "contradicts" | "insufficient_evidence";
|
|
30
|
+
confidence: number;
|
|
31
|
+
probabilities: {
|
|
32
|
+
supports: number;
|
|
33
|
+
contradicts: number;
|
|
34
|
+
insufficient_evidence: number;
|
|
35
|
+
};
|
|
36
|
+
status: "ok";
|
|
37
|
+
} | {
|
|
38
|
+
status: "unavailable";
|
|
39
|
+
needsReview: boolean;
|
|
40
|
+
reason: string;
|
|
41
|
+
}>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
2
|
+
import { abortable } from "./abortable.js";
|
|
3
|
+
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
4
|
+
/** Review the injected blurb using only exact indexed references already on its claims. */
|
|
5
|
+
export async function reviewPersonDossier(params) {
|
|
6
|
+
const unavailable = (reason) => ({ status: "unavailable", needsReview: true, reason });
|
|
7
|
+
const { config, runtime, active, person, dossier } = params;
|
|
8
|
+
if (!config.peoplePrimer.enabled || !config.typesafe.enabled) {
|
|
9
|
+
return unavailable("TypeSafe background review is disabled; verify manually before saving");
|
|
10
|
+
}
|
|
11
|
+
const citations = new Map();
|
|
12
|
+
for (const section of dossier.sections)
|
|
13
|
+
for (const claim of section.claims)
|
|
14
|
+
for (const ref of claim.evidence) {
|
|
15
|
+
const match = /^(qmd:\/\/[^#]+)#L([1-9]\d*)(?:-L([1-9]\d*))?$/.exec(ref.locator);
|
|
16
|
+
if (!match)
|
|
17
|
+
return unavailable("Evidence needs exact qmd://path#Lstart-Lend references or explicit manual verification");
|
|
18
|
+
const from = Number(match[2]), end = Number(match[3] ?? match[2]);
|
|
19
|
+
if (!Number.isSafeInteger(from) || !Number.isSafeInteger(end) || end < from || end - from >= 120) {
|
|
20
|
+
return unavailable("Evidence ranges must contain 1–120 lines");
|
|
21
|
+
}
|
|
22
|
+
const citation = { path: match[1], from, lines: end - from + 1 };
|
|
23
|
+
citations.set(JSON.stringify(citation), citation);
|
|
24
|
+
}
|
|
25
|
+
if (!citations.size || citations.size > 3)
|
|
26
|
+
return unavailable("Choose 1–3 distinct indexed evidence ranges or verify manually");
|
|
27
|
+
const deadline = AbortSignal.timeout(120_000);
|
|
28
|
+
const signal = params.signal ? AbortSignal.any([params.signal, deadline]) : deadline;
|
|
29
|
+
try {
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
const apiKey = await abortable(resolveTypeSafeApiKey(config.typesafe), signal);
|
|
32
|
+
if (!apiKey)
|
|
33
|
+
return unavailable("TypeSafe API key not configured; verify manually before saving");
|
|
34
|
+
const { manager } = await abortable(runtime.getMemorySearchManager(active), signal);
|
|
35
|
+
if (!manager)
|
|
36
|
+
return unavailable("Memory index unavailable; verify manually before saving");
|
|
37
|
+
return await abortable(manager.reviewClaim({
|
|
38
|
+
claim: dossier.blurb, citations: [...citations.values()],
|
|
39
|
+
personBackground: { name: person.preferredName ?? person.displayName,
|
|
40
|
+
agentName: resolveAgentIdentity(active.cfg, active.agentId)?.name?.trim() || params.agentName?.trim() || "the assistant" },
|
|
41
|
+
corpora: config.peoplePrimer.corpora, apiKey, timeoutMs: config.peoplePrimer.timeoutMs, signal,
|
|
42
|
+
}), signal);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return unavailable("Background review failed or was cancelled; no dossier written");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Type, type Static } from "typebox";
|
|
2
|
+
import type { CorpusConfig } from "./config.js";
|
|
3
|
+
export declare const peoplePrimerSchema: Type.TObject<{
|
|
4
|
+
enabled: Type.TBoolean;
|
|
5
|
+
corpora: Type.TArray<Type.TString>;
|
|
6
|
+
hitsPerQuestion: Type.TInteger;
|
|
7
|
+
minScore: Type.TNumber;
|
|
8
|
+
minUsefulness: Type.TNumber;
|
|
9
|
+
maxEvidencePerQuestion: Type.TInteger;
|
|
10
|
+
timeoutMs: Type.TInteger;
|
|
11
|
+
}>;
|
|
12
|
+
export type PeoplePrimerConfig = Static<typeof peoplePrimerSchema>;
|
|
13
|
+
export declare function resolvePeoplePrimer(value: unknown, corpora: readonly CorpusConfig[], peopleEnabled: boolean): PeoplePrimerConfig;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
export const peoplePrimerSchema = Type.Object({
|
|
4
|
+
enabled: Type.Boolean({ default: false }),
|
|
5
|
+
corpora: Type.Array(Type.String({ minLength: 1 }), { default: [] }),
|
|
6
|
+
hitsPerQuestion: Type.Integer({ minimum: 1, maximum: 40, default: 30 }),
|
|
7
|
+
minScore: Type.Number({ minimum: 0, maximum: 1, default: 0.35 }),
|
|
8
|
+
minUsefulness: Type.Number({ minimum: 0.5, maximum: 1, default: 0.8 }),
|
|
9
|
+
maxEvidencePerQuestion: Type.Integer({ minimum: 1, maximum: 10, default: 3 }),
|
|
10
|
+
timeoutMs: Type.Integer({ minimum: 1, maximum: 60000, default: 30000 }),
|
|
11
|
+
}, { additionalProperties: false });
|
|
12
|
+
export function resolvePeoplePrimer(value, corpora, peopleEnabled) {
|
|
13
|
+
if (value === undefined)
|
|
14
|
+
return { enabled: false, corpora: [], hitsPerQuestion: 30,
|
|
15
|
+
minScore: 0.35, minUsefulness: 0.8, maxEvidencePerQuestion: 3, timeoutMs: 30000 };
|
|
16
|
+
let config;
|
|
17
|
+
try {
|
|
18
|
+
const withDefaults = Value.Default(peoplePrimerSchema, value);
|
|
19
|
+
if (!Value.Check(peoplePrimerSchema, withDefaults))
|
|
20
|
+
throw new Error("Invalid config");
|
|
21
|
+
config = withDefaults;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error("Invalid peoplePrimer configuration");
|
|
25
|
+
}
|
|
26
|
+
if (config.corpora.some(name => !corpora.some(c => c.name === name && c.kind !== "skills"))) {
|
|
27
|
+
throw new Error("peoplePrimer.corpora must list configured non-skill corpora");
|
|
28
|
+
}
|
|
29
|
+
if (config.enabled && (!peopleEnabled || !config.corpora.length)) {
|
|
30
|
+
throw new Error("peoplePrimer requires people.enabled and explicit approved corpora");
|
|
31
|
+
}
|
|
32
|
+
return { ...config, corpora: [...new Set(config.corpora)] };
|
|
33
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
2
|
+
import type { UnblockMemoryConfig } from "./config.js";
|
|
3
|
+
import type { PeopleStores } from "./people-store.js";
|
|
4
|
+
import type { QmdMemoryRuntime } from "./runtime.js";
|
|
5
|
+
export declare function registerPeoplePrimerTool(api: OpenClawPluginApi, runtime: QmdMemoryRuntime, stores: PeopleStores, config: UnblockMemoryConfig): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Type } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
|
+
import { jsonResult, resolveAgentIdentity } from "openclaw/plugin-sdk/agent-runtime";
|
|
4
|
+
import { getContext } from "./tool-context.js";
|
|
5
|
+
import { resolveTypeSafeApiKey } from "./typesafe.js";
|
|
6
|
+
import { primePersonDossier } from "./people-primer.js";
|
|
7
|
+
import { abortable } from "./abortable.js";
|
|
8
|
+
import { backgroundWordCount, PEOPLE_BACKGROUND_MAX_WORDS } from "./people-background.js";
|
|
9
|
+
const parameters = Type.Object({
|
|
10
|
+
personId: Type.String({ pattern: "\\S", maxLength: 1000 }),
|
|
11
|
+
agentName: Type.Optional(Type.String({ pattern: "\\S", maxLength: 100,
|
|
12
|
+
description: "Your human-facing name, e.g. Bill, when no agent identity name is configured. Never a person's name guessed from search results." })),
|
|
13
|
+
draft: Type.Optional(Type.Object({
|
|
14
|
+
blurb: Type.String({ pattern: "\\S", maxLength: 1200, description: "Final background-only snippet, at most 70 words. Supplying a draft reviews it instead of running searches." }),
|
|
15
|
+
citations: Type.Array(Type.Object({
|
|
16
|
+
path: Type.String({ pattern: "^qmd://", maxLength: 2000 }),
|
|
17
|
+
from: Type.Integer({ minimum: 1 }), lines: Type.Integer({ minimum: 1, maximum: 120 }),
|
|
18
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 3 }),
|
|
19
|
+
}, { additionalProperties: false })),
|
|
20
|
+
}, { additionalProperties: false });
|
|
21
|
+
export function registerPeoplePrimerTool(api, runtime, stores, config) {
|
|
22
|
+
const running = new Set();
|
|
23
|
+
api.registerTool(ctx => {
|
|
24
|
+
const active = getContext(ctx);
|
|
25
|
+
if (!active)
|
|
26
|
+
return null;
|
|
27
|
+
return {
|
|
28
|
+
name: "memory_people_prime", label: "Prime Person Dossier",
|
|
29
|
+
description: "Prepare background-only evidence using three identity/organization/agent-relationship questions. Supply draft to review a final <=70-word snippet against indexed citations instead of searching. Requires peoplePrimer opt-in and approved corpora. Sends identity, approved excerpts and optional draft to TypeSafe, never the existing dossier. Advisory; never writes dossiers.",
|
|
30
|
+
parameters,
|
|
31
|
+
async execute(_id, params, signal) {
|
|
32
|
+
const { personId, agentName: suppliedName, draft } = Value.Parse(parameters, params);
|
|
33
|
+
if (!config.people.enabled || !config.peoplePrimer.enabled || !config.typesafe.enabled)
|
|
34
|
+
return jsonResult({ status: "disabled" });
|
|
35
|
+
if (draft && backgroundWordCount(draft.blurb) > PEOPLE_BACKGROUND_MAX_WORDS) {
|
|
36
|
+
return jsonResult({ status: "invalid", needsReview: true, reason: "Background snippet must not exceed 70 words" });
|
|
37
|
+
}
|
|
38
|
+
const key = JSON.stringify([active.agentId, personId]);
|
|
39
|
+
if (running.has(key))
|
|
40
|
+
return jsonResult({ status: "busy" });
|
|
41
|
+
running.add(key);
|
|
42
|
+
const deadline = AbortSignal.timeout(120_000);
|
|
43
|
+
const combined = signal ? AbortSignal.any([signal, deadline]) : deadline;
|
|
44
|
+
try {
|
|
45
|
+
combined.throwIfAborted();
|
|
46
|
+
const apiKey = await abortable(resolveTypeSafeApiKey(config.typesafe), combined);
|
|
47
|
+
combined.throwIfAborted();
|
|
48
|
+
if (!apiKey)
|
|
49
|
+
return jsonResult({ status: "unavailable", reason: "TypeSafe API key not configured" });
|
|
50
|
+
const store = stores.get(active.agentId);
|
|
51
|
+
const person = store.getPerson(personId);
|
|
52
|
+
if (!person || person.status !== "active")
|
|
53
|
+
return jsonResult({ status: "not_found" });
|
|
54
|
+
const identities = store.listIdentities(personId);
|
|
55
|
+
if (identities.length && identities.every(i => i.isBot === true || i.isDeactivated))
|
|
56
|
+
return jsonResult({ status: "unavailable", reason: "No active human identity" });
|
|
57
|
+
const { manager } = await abortable(runtime.getMemorySearchManager(active), combined);
|
|
58
|
+
combined.throwIfAborted();
|
|
59
|
+
if (!manager)
|
|
60
|
+
return jsonResult({ status: "unavailable", reason: "Memory unavailable" });
|
|
61
|
+
const agentName = resolveAgentIdentity(active.cfg, active.agentId)?.name?.trim() || suppliedName?.trim() || "the assistant";
|
|
62
|
+
if (draft)
|
|
63
|
+
return jsonResult(await manager.reviewClaim({ claim: draft.blurb, citations: draft.citations,
|
|
64
|
+
personBackground: { name: person.preferredName ?? person.displayName, agentName },
|
|
65
|
+
corpora: config.peoplePrimer.corpora, apiKey, timeoutMs: config.peoplePrimer.timeoutMs, signal: combined }));
|
|
66
|
+
return jsonResult(await primePersonDossier({ personId, agentName, store, config: config.peoplePrimer, apiKey, signal: combined,
|
|
67
|
+
search: (query, options) => manager.search(query, { ...options, requestContext: active.requestContext }) }));
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return jsonResult({ status: "unavailable", needsReview: true, reason: "Primer failed or was cancelled; no dossier written" });
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
running.delete(key);
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}, { names: ["memory_people_prime"] });
|
|
78
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { CorpusMemorySearchResult, CorpusSearchOptions } from "./contracts.js";
|
|
2
|
+
import type { PeoplePrimerConfig } from "./people-primer-config.js";
|
|
3
|
+
import type { PeopleStore } from "./people-store.js";
|
|
4
|
+
/** Evidence preparation only. Search is local; approved excerpts go to TypeSafe.
|
|
5
|
+
* No identity inference, generated claims, dossier writes or automatic injection. */
|
|
6
|
+
export declare function primePersonDossier(params: {
|
|
7
|
+
personId: string;
|
|
8
|
+
agentName: string;
|
|
9
|
+
store: Pick<PeopleStore, "getPerson" | "listIdentities" | "getPrimerJudgment" | "cachePrimerJudgment">;
|
|
10
|
+
search: (query: string, options: CorpusSearchOptions) => Promise<CorpusMemorySearchResult[]>;
|
|
11
|
+
config: PeoplePrimerConfig;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
status: "disabled";
|
|
16
|
+
reason?: undefined;
|
|
17
|
+
personId?: undefined;
|
|
18
|
+
name?: undefined;
|
|
19
|
+
version?: undefined;
|
|
20
|
+
advisory?: undefined;
|
|
21
|
+
stats?: undefined;
|
|
22
|
+
questions?: undefined;
|
|
23
|
+
evidence?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
status: "not_found";
|
|
26
|
+
reason?: undefined;
|
|
27
|
+
personId?: undefined;
|
|
28
|
+
name?: undefined;
|
|
29
|
+
version?: undefined;
|
|
30
|
+
advisory?: undefined;
|
|
31
|
+
stats?: undefined;
|
|
32
|
+
questions?: undefined;
|
|
33
|
+
evidence?: undefined;
|
|
34
|
+
} | {
|
|
35
|
+
status: "unavailable";
|
|
36
|
+
reason: string;
|
|
37
|
+
personId?: undefined;
|
|
38
|
+
name?: undefined;
|
|
39
|
+
version?: undefined;
|
|
40
|
+
advisory?: undefined;
|
|
41
|
+
stats?: undefined;
|
|
42
|
+
questions?: undefined;
|
|
43
|
+
evidence?: undefined;
|
|
44
|
+
} | {
|
|
45
|
+
status: "ok" | "partial";
|
|
46
|
+
personId: string;
|
|
47
|
+
name: string;
|
|
48
|
+
version: string;
|
|
49
|
+
advisory: string;
|
|
50
|
+
stats: {
|
|
51
|
+
uniqueCandidates: number;
|
|
52
|
+
requests: number;
|
|
53
|
+
cached: number;
|
|
54
|
+
failed: number;
|
|
55
|
+
elapsedMs: number;
|
|
56
|
+
};
|
|
57
|
+
questions: {
|
|
58
|
+
graded: number;
|
|
59
|
+
qualifying: number;
|
|
60
|
+
coverage: "unknown" | "evidence_found" | "uncertain";
|
|
61
|
+
evidence: {
|
|
62
|
+
evidenceId: string;
|
|
63
|
+
vectorScore: number;
|
|
64
|
+
usefulness: number;
|
|
65
|
+
aboutPerson: number;
|
|
66
|
+
explicitBackground: number;
|
|
67
|
+
enduring: number;
|
|
68
|
+
recognition: number;
|
|
69
|
+
}[];
|
|
70
|
+
review: {
|
|
71
|
+
evidenceId: string;
|
|
72
|
+
vectorScore: number;
|
|
73
|
+
usefulness: number;
|
|
74
|
+
aboutPerson: number;
|
|
75
|
+
explicitBackground: number;
|
|
76
|
+
enduring: number;
|
|
77
|
+
recognition: number;
|
|
78
|
+
}[];
|
|
79
|
+
retrieved?: number | undefined;
|
|
80
|
+
eligible?: number | undefined;
|
|
81
|
+
oversized?: number | undefined;
|
|
82
|
+
id: string;
|
|
83
|
+
question: string;
|
|
84
|
+
}[];
|
|
85
|
+
evidence: {
|
|
86
|
+
id: string;
|
|
87
|
+
path: string;
|
|
88
|
+
from: number;
|
|
89
|
+
lines: number;
|
|
90
|
+
excerpt: string;
|
|
91
|
+
corpus: string;
|
|
92
|
+
}[];
|
|
93
|
+
reason?: undefined;
|
|
94
|
+
}>;
|