@nelsonlongxiang/dsh-open-a2a-net 0.6.30 → 0.6.32
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 +4 -1
- package/README_zh.md +3 -0
- package/assets/nexusDist/assets/index-D5H8R8de.js +9 -0
- package/assets/nexusDist/assets/index-D9GYZyGZ.css +1 -0
- package/assets/nexusDist/index.html +2 -2
- package/cli/a2a.mjs +445 -57
- package/lib/a2a-client.js +9 -4
- package/lib/auth-observer.js +254 -0
- package/lib/blob-store.js +210 -0
- package/lib/card.js +4 -2
- package/lib/cli-deposit.js +122 -7
- package/lib/client.js +64 -64
- package/lib/client.js.map +1 -1
- package/lib/faces/admin.js +133 -43
- package/lib/faces/canvas-notify.js +12 -3
- package/lib/faces/files.js +155 -0
- package/lib/faces/nodes.js +171 -0
- package/lib/faces/session-log.js +162 -0
- package/lib/faces/wire.js +206 -39
- package/lib/index.js +809 -1720
- package/lib/joined-store.js +23 -0
- package/lib/ledger-conservation.js +97 -0
- package/lib/legacy/teams-bridge-inbound.js +156 -0
- package/lib/node-bind.js +534 -0
- package/lib/node-github.js +72 -0
- package/lib/node-self.js +453 -0
- package/lib/node-store.js +484 -0
- package/lib/node-tasks.js +342 -0
- package/lib/origin-claim.js +246 -0
- package/lib/peer-directory.js +134 -0
- package/lib/receipt.js +26 -1
- package/lib/skill/SKILL.md +3 -1
- package/lib/skill/references/cli.md +82 -3
- package/lib/skill/references/collaboration.md +146 -73
- package/lib/skill/references/discovery.md +29 -0
- package/lib/skill/references/intake.md +107 -0
- package/lib/skill/references/router.md +96 -0
- package/lib/skill/references/setup.md +2 -1
- package/lib/task-auth.js +483 -0
- package/lib/task-ledger.js +324 -12
- package/lib/team-store.js +24 -1
- package/lib/tools/act.js +383 -0
- package/lib/tools/files.js +201 -0
- package/lib/tools/info.js +684 -0
- package/lib/types/a2a-client.d.ts +10 -4
- package/lib/types/auth-observer.d.ts +110 -0
- package/lib/types/blob-store.d.ts +83 -0
- package/lib/types/cli-deposit.d.ts +38 -0
- package/lib/types/faces/admin.d.ts +14 -2
- package/lib/types/faces/canvas-notify.d.ts +4 -1
- package/lib/types/faces/files.d.ts +53 -0
- package/lib/types/faces/nodes.d.ts +69 -0
- package/lib/types/faces/session-log.d.ts +44 -0
- package/lib/types/faces/wire.d.ts +50 -5
- package/lib/types/index.d.ts +27 -0
- package/lib/types/joined-store.d.ts +9 -0
- package/lib/types/ledger-conservation.d.ts +77 -0
- package/lib/types/legacy/teams-bridge-inbound.d.ts +94 -0
- package/lib/types/node-bind.d.ts +220 -0
- package/lib/types/node-github.d.ts +35 -0
- package/lib/types/node-self.d.ts +178 -0
- package/lib/types/node-store.d.ts +180 -0
- package/lib/types/node-tasks.d.ts +121 -0
- package/lib/types/origin-claim.d.ts +119 -0
- package/lib/types/peer-directory.d.ts +39 -0
- package/lib/types/receipt.d.ts +12 -0
- package/lib/types/task-auth.d.ts +271 -0
- package/lib/types/task-ledger.d.ts +143 -6
- package/lib/types/team-store.d.ts +9 -0
- package/lib/types/tools/act.d.ts +52 -0
- package/lib/types/tools/files.d.ts +38 -0
- package/lib/types/tools/info.d.ts +102 -0
- package/lib/types/types.d.ts +6 -0
- package/nexus-stage/src/canvas-ops.ts +17 -2
- package/nexus-stage/src/canvas-wire.ts +27 -19
- package/nexus-stage/src/federation.ts +1 -0
- package/nexus-stage/src/main.ts +41 -18
- package/nexus-stage/src/planning-member-handles.ts +34 -0
- package/nexus-stage/src/planning-mirror.ts +47 -3
- package/nexus-stage/src/planning-reachability.ts +28 -0
- package/nexus-stage/src/planning-types.ts +1 -1
- package/nexus-stage/src/planning-view.ts +30 -27
- package/nexus-stage/src/planning.css +4 -0
- package/nexus-stage/src/wb-directory.ts +13 -0
- package/nexus-stage/src/workbench.css +3 -0
- package/nexus-stage/src/workbench.ts +5 -1
- package/package.json +1 -1
- package/assets/nexusDist/assets/index-C_x3iAth.js +0 -9
- package/assets/nexusDist/assets/index-Dz8N80tI.css +0 -1
package/lib/types/index.d.ts
CHANGED
|
@@ -19,6 +19,8 @@ export declare function resolveCallerLabel(input: {
|
|
|
19
19
|
sessionLabel?: string;
|
|
20
20
|
}): string | undefined;
|
|
21
21
|
import { type ReceiptResolvedInfo } from './task-ledger.ts';
|
|
22
|
+
import { type AffectedEntry } from './auth-observer.ts';
|
|
23
|
+
import { type SettlementMode } from './task-auth.ts';
|
|
22
24
|
import type { Context } from '@deepseek-ai/cordis';
|
|
23
25
|
import s from '@deepseek-ai/schemastery';
|
|
24
26
|
declare module '@deepseek-ai/dsh-llm' {
|
|
@@ -42,6 +44,8 @@ export type { ReceiptResolvedInfo } from './task-ledger.ts';
|
|
|
42
44
|
declare module '@deepseek-ai/cordis' {
|
|
43
45
|
interface Events {
|
|
44
46
|
'a2a/receipt-resolved'(payload: ReceiptResolvedInfo): void;
|
|
47
|
+
/** #114 S6: an allowlist change left unsettled tasks on another footing (observe mode — nothing is cancelled). */
|
|
48
|
+
'a2a/authorization-affected'(payload: AffectedEntry): void;
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
export declare const name = "a2a";
|
|
@@ -70,6 +74,8 @@ export interface Config {
|
|
|
70
74
|
readonly team: string;
|
|
71
75
|
/** S4 fix 3: audit (default) or refuse host-side (agent-less) routing calls. */
|
|
72
76
|
readonly gateAgentLessCalls: 'audit' | 'refuse';
|
|
77
|
+
/** #114 S4: settlement authorization — off, observe (record would-refuse, settle as before), enforce. */
|
|
78
|
+
readonly settlementAuth: SettlementMode;
|
|
73
79
|
/**
|
|
74
80
|
* Publish this node's `/.well-known/agent-card.json` on the shared web
|
|
75
81
|
* server (peer discovery). Requires the web server in the composition;
|
|
@@ -77,6 +83,14 @@ export interface Config {
|
|
|
77
83
|
* joined session teams so peers find it without a central directory.
|
|
78
84
|
*/
|
|
79
85
|
readonly announce: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* gh#193: send nothing to any peer. Every outbound peer request — card
|
|
88
|
+
* reads, directory sweeps, routes, log reads, canvas pings and canvas-op
|
|
89
|
+
* relays — goes through one fetch that refuses while this is on, so a
|
|
90
|
+
* test host stays dark even with learned peers in `a2a/peers.json`
|
|
91
|
+
* (which is left untouched). Inbound faces keep answering. Default false.
|
|
92
|
+
*/
|
|
93
|
+
readonly isolated: boolean;
|
|
80
94
|
/** Human-facing name published on the agent card. */
|
|
81
95
|
readonly agentName: string;
|
|
82
96
|
/**
|
|
@@ -103,6 +117,19 @@ export interface Config {
|
|
|
103
117
|
* self-asserted already-linked name must not pass on shape alone).
|
|
104
118
|
*/
|
|
105
119
|
readonly originClaimWindow: boolean;
|
|
120
|
+
/**
|
|
121
|
+
* OriginClaim unbound window (docs/protocol/delivery-origin-auth.md §2.1):
|
|
122
|
+
* while true (default), this host also signs the legacy unbound form and
|
|
123
|
+
* accepts a peer's unbound-only envelope (counted as unboundInbound);
|
|
124
|
+
* closed, it signs and accepts the audience-bound form only.
|
|
125
|
+
*/
|
|
126
|
+
readonly originClaimUnboundWindow: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Extra origins this host answers to as an OriginClaim audience, beyond
|
|
129
|
+
* its card URL and the address a request arrives on (port-mapped or
|
|
130
|
+
* proxied hosts name the URL peers dial).
|
|
131
|
+
*/
|
|
132
|
+
readonly originClaimAudiences: string[];
|
|
106
133
|
/**
|
|
107
134
|
* #19 D2: card-face sessionTeams excerpt exposure — 'full' | 'masked'
|
|
108
135
|
* | 'off' (owner-level; default full). See the schema comment.
|
|
@@ -28,6 +28,7 @@ export declare class JoinedSessions {
|
|
|
28
28
|
private readonly path;
|
|
29
29
|
/** Session ids, most recently joined first. */
|
|
30
30
|
private ids;
|
|
31
|
+
private readonly listeners;
|
|
31
32
|
/**
|
|
32
33
|
* @param path - persistence file (`<dsh-home>/a2a/joined.json`); empty = no persistence.
|
|
33
34
|
*/
|
|
@@ -54,6 +55,14 @@ export declare class JoinedSessions {
|
|
|
54
55
|
* @param id - the session id whose intent is removed.
|
|
55
56
|
*/
|
|
56
57
|
remove(id: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* #114 S6: observe changes — the listener runs after every write that
|
|
60
|
+
* changed the store (never on restore or reload). A throwing listener
|
|
61
|
+
* cannot break the write.
|
|
62
|
+
* @returns the unsubscribe function.
|
|
63
|
+
*/
|
|
64
|
+
onChange(listener: () => void): () => void;
|
|
65
|
+
private notify;
|
|
57
66
|
/**
|
|
58
67
|
* F7 (wake-intent self-heal): re-read the persisted snapshot into memory.
|
|
59
68
|
* A boot-time transient (e.g. the workspace registry briefly reporting a
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ledger's conservation machinery (gh#154): four lifetime counters plus
|
|
3
|
+
* the durable damage marker, moved verbatim out of TaskLedger so each module
|
|
4
|
+
* stays under the R-mod cap. Counting semantics live here; the ledger's
|
|
5
|
+
* track / archiveSettled / restore / persist seams delegate.
|
|
6
|
+
*
|
|
7
|
+
* Identity: trackedTotal = |owed| + Σ archivedByEnd + restoreTrimmed.
|
|
8
|
+
* Duplicates are checked by the ledger (it owns the rows); counters only
|
|
9
|
+
* answer the count half. A persisted damage marker never clears on its own —
|
|
10
|
+
* only an explicit reseed policy may clear it (none exists by design).
|
|
11
|
+
* @module src/ledger-conservation
|
|
12
|
+
*/
|
|
13
|
+
import type { ArchiveEnd } from './task-ledger.ts';
|
|
14
|
+
/** The persisted conservation block (rides the ledger snapshot; absent = pre-#154 legacy). */
|
|
15
|
+
export interface ConservationSnapshot {
|
|
16
|
+
readonly trackedTotal: number;
|
|
17
|
+
readonly archivedByEnd: Record<ArchiveEnd, number>;
|
|
18
|
+
readonly archiveTrimmed: number;
|
|
19
|
+
readonly restoreTrimmed: number;
|
|
20
|
+
/** review r2: a damage marker PERSISTS — restarts keep refusing conserved until an explicit reseed clears it. */
|
|
21
|
+
readonly damaged?: string;
|
|
22
|
+
}
|
|
23
|
+
/** One full conservation verdict (the /state projection). */
|
|
24
|
+
export interface ConservationVerdict {
|
|
25
|
+
conserved: boolean;
|
|
26
|
+
delta: number;
|
|
27
|
+
trackedTotal: number;
|
|
28
|
+
owed: number;
|
|
29
|
+
archivedByEnd: Record<ArchiveEnd, number>;
|
|
30
|
+
archivedTotal: number;
|
|
31
|
+
archiveTrimmed: number;
|
|
32
|
+
restoreTrimmed: number;
|
|
33
|
+
duplicateTaskIds: string[];
|
|
34
|
+
damaged?: string;
|
|
35
|
+
}
|
|
36
|
+
/** The archive-row shape the bootstrap classifier reads (endedBy optional pre-gh#31; generic so the ledger passes its own row type). */
|
|
37
|
+
export type ArchiveClassifiable = {
|
|
38
|
+
readonly endedBy?: ArchiveEnd;
|
|
39
|
+
};
|
|
40
|
+
/** The parsed outcome of restoring counters from one snapshot. */
|
|
41
|
+
export type ConservationRestore = {
|
|
42
|
+
readonly kind: 'legacy';
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: 'valid';
|
|
45
|
+
readonly snapshot: ConservationSnapshot;
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: 'damaged';
|
|
48
|
+
readonly reason: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Restore-classify one snapshot's conservation block:
|
|
52
|
+
* - absent block → legacy bootstrap from what the rows prove;
|
|
53
|
+
* - present, fully valid (every field a non-negative integer, archiveTrimmed
|
|
54
|
+
* bounded by the lifetime archived total, optional damaged marker a
|
|
55
|
+
* non-empty string) → valid, counters load as persisted;
|
|
56
|
+
* - anything else (partial, negative, fractional, null body, unbounded trim)
|
|
57
|
+
* → damaged: counters stay zero and the verdict refuses conserved.
|
|
58
|
+
*/
|
|
59
|
+
export declare function classifyConservationBlock(raw: unknown): ConservationRestore;
|
|
60
|
+
/** The counters themselves — one mutable holder the ledger owns. */
|
|
61
|
+
export declare class ConservationCounters {
|
|
62
|
+
trackedTotal: number;
|
|
63
|
+
readonly archivedByEnd: Record<ArchiveEnd, number>;
|
|
64
|
+
archiveTrimmed: number;
|
|
65
|
+
restoreTrimmed: number;
|
|
66
|
+
damaged: string | undefined;
|
|
67
|
+
/** Legacy bootstrap: the kept rows plus this load's cuts are all the snapshot proves. */
|
|
68
|
+
legacyBootstrap<T extends {
|
|
69
|
+
readonly endedBy?: ArchiveEnd;
|
|
70
|
+
}>(owedCount: number, archive: readonly T[], restoreTrimmed: number, classify: (row: T) => ArchiveEnd): void;
|
|
71
|
+
/** Valid-block load: counters EXACTLY as persisted (review P2: an undercount stays red — never repaired upward); restoreTrimmed ADDS this load's cuts to the persisted lifetime (review P2: neither overwrite nor max). */
|
|
72
|
+
loadFrom(snapshot: ConservationSnapshot, owedCount: number, archiveCount: number): void;
|
|
73
|
+
/** The persisted shape (damaged rides along once stamped). */
|
|
74
|
+
toSnapshot(): ConservationSnapshot;
|
|
75
|
+
/** The verdict's count half (the ledger supplies rows, owed count, duplicates). */
|
|
76
|
+
verdict(owedCount: number, archiveCount: number, duplicateTaskIds: string[]): ConservationVerdict;
|
|
77
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native-teams inbound bridge (gh#77 G-segment, inbound half), moved
|
|
3
|
+
* verbatim out of apply() in src/index.ts: the structural registry probe, the
|
|
4
|
+
* memoized claim check, prepare (claim + seam + parent), and the bounded
|
|
5
|
+
* round. Opt-in behind config.nativeTeamsInbound; when the sibling
|
|
6
|
+
* @nelsonlongxiang/dsh-native-teams plugin is not composed every probe
|
|
7
|
+
* declines and the standard resolution chain keeps its verdict.
|
|
8
|
+
*
|
|
9
|
+
* The OUTBOUND bridgeFace stays with the dispatch trunk in index.ts until the
|
|
10
|
+
* G-out slice - it submits through the trunk's candidate walk (bidirectional
|
|
11
|
+
* coupling), which is why #77 prescribes taking G in pieces.
|
|
12
|
+
* @module src/legacy/teams-bridge-inbound
|
|
13
|
+
*/
|
|
14
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
16
|
+
import type { Config } from '../index.ts';
|
|
17
|
+
/** Structural descriptor of one team name across planes (the registry's answer). */
|
|
18
|
+
export interface BridgeTeamsDescriptor {
|
|
19
|
+
readonly plane: 'local' | 'a2a';
|
|
20
|
+
readonly ambiguous?: boolean;
|
|
21
|
+
readonly localLabel?: string;
|
|
22
|
+
}
|
|
23
|
+
/** The sibling registry's widened structural face (presence-guarded probe). */
|
|
24
|
+
export interface BridgeTeamsRegistry {
|
|
25
|
+
listTeams(): Array<{
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
}>;
|
|
29
|
+
describeTarget(handle: string): Promise<BridgeTeamsDescriptor>;
|
|
30
|
+
startRound(args: {
|
|
31
|
+
team?: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}, parent: {
|
|
34
|
+
id: string;
|
|
35
|
+
session: {
|
|
36
|
+
events?: readonly unknown[];
|
|
37
|
+
};
|
|
38
|
+
}, signal: AbortSignal): Promise<string>;
|
|
39
|
+
}
|
|
40
|
+
/** A prepared dispatch: claim verified, seam live, parent initiator found. */
|
|
41
|
+
export type NativeTeamsPrepared = {
|
|
42
|
+
readonly ok: true;
|
|
43
|
+
readonly teams: BridgeTeamsRegistry;
|
|
44
|
+
readonly parent: {
|
|
45
|
+
id: string;
|
|
46
|
+
session: {
|
|
47
|
+
events?: readonly unknown[];
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
} | {
|
|
51
|
+
readonly ok: false;
|
|
52
|
+
readonly reason: 'not-claimed';
|
|
53
|
+
} | {
|
|
54
|
+
readonly ok: false;
|
|
55
|
+
readonly reason: 'error';
|
|
56
|
+
readonly error: string;
|
|
57
|
+
};
|
|
58
|
+
/** The bounded round's outcome (settled / unsettled-deadline / failed / aborted). */
|
|
59
|
+
export type NativeTeamsRoundOutcome = {
|
|
60
|
+
readonly ok: true;
|
|
61
|
+
readonly reply: string;
|
|
62
|
+
readonly settled: boolean;
|
|
63
|
+
} | {
|
|
64
|
+
readonly ok: false;
|
|
65
|
+
readonly error: string;
|
|
66
|
+
readonly phase?: 'pre-dispatch';
|
|
67
|
+
readonly aborted?: boolean;
|
|
68
|
+
};
|
|
69
|
+
/** Everything the inbound bridge captures from apply(). */
|
|
70
|
+
export interface TeamsBridgeInboundDeps {
|
|
71
|
+
readonly ctx: Context;
|
|
72
|
+
readonly config: Config;
|
|
73
|
+
readonly liveAgent: () => Agent | undefined;
|
|
74
|
+
readonly fromClause: (from: string) => string;
|
|
75
|
+
readonly sanitizeFromClause: (from: string) => string;
|
|
76
|
+
readonly proofHeader?: (team: string, taskId: string, from: string, proofClause: string) => string;
|
|
77
|
+
/** N4 session taint: the round about to carry remote text runs under this parent (its members derive from it). */
|
|
78
|
+
readonly taint?: (agent: Agent) => void;
|
|
79
|
+
}
|
|
80
|
+
/** The four inbound-bridge operations, verbatim from apply(). */
|
|
81
|
+
export interface TeamsBridgeInbound {
|
|
82
|
+
teamsRegistry(): BridgeTeamsRegistry | undefined;
|
|
83
|
+
nativeTeamsClaims(team: string): Promise<boolean>;
|
|
84
|
+
nativeTeamsPrepare(team: string): Promise<NativeTeamsPrepared>;
|
|
85
|
+
nativeTeamsRound(prepared: Extract<NativeTeamsPrepared, {
|
|
86
|
+
ok: true;
|
|
87
|
+
}>, team: string, caller: string, message: string, taskId: string | undefined, proofClause: string, external?: AbortSignal, proofHeader?: (team: string, taskId: string, from: string, proofClause: string) => string): Promise<NativeTeamsRoundOutcome>;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build the inbound bridge. The bodies are byte-identical to their apply()
|
|
91
|
+
* originals (same two-space indent); only the four capture points arrive as
|
|
92
|
+
* deps.
|
|
93
|
+
*/
|
|
94
|
+
export declare function createTeamsBridgeInbound(deps: TeamsBridgeInboundDeps): TeamsBridgeInbound;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session ↔ node binding (docs/design/node-identity.md, slices N2+N3).
|
|
3
|
+
*
|
|
4
|
+
* - A binding ties one JOINED session to one node (§3): a session carries
|
|
5
|
+
* at most one node, a node may have several live sessions. The table is
|
|
6
|
+
* `node-bindings.json` under the a2a home; it survives restarts and
|
|
7
|
+
* cooling unloads (`agent/disposed` is not the end of a binding).
|
|
8
|
+
* - A binding ends when the session leaves the network (leave, or its join
|
|
9
|
+
* intent falls off JOIN_CAP), or when its node is archived: the row is
|
|
10
|
+
* deleted and the sections come off. An ARCHIVED session never mounts and
|
|
11
|
+
* cannot bind; its row is kept, like its join intent (the 3081 lesson: a
|
|
12
|
+
* transient archived reading must not destroy durable intent).
|
|
13
|
+
* - Mounts belong to the session scope, not to this plugin: the plugin
|
|
14
|
+
* disposes every mount it made when it unloads (`disposeAll`), so a
|
|
15
|
+
* reloaded plugin never meets registrations it cannot see.
|
|
16
|
+
* - Binding mounts at once (§3 "绑定即生效"): two prompt sections are
|
|
17
|
+
* registered on that session's own `agent.ctx` — the mutable layer at
|
|
18
|
+
* 10150, the core layer after it at 10180 with an explicit precedence
|
|
19
|
+
* line, both before the deployment suffix at 10200 (§5).
|
|
20
|
+
* - Frozen snapshot (§5): the texts are read once, when the session mounts.
|
|
21
|
+
* A layer written later takes effect from the session's next mount
|
|
22
|
+
* (resume / clear / compact re-publish it), never mid-session: the binding
|
|
23
|
+
* row records the versions each mount froze, and a plugin reload re-mounts
|
|
24
|
+
* live sessions at them (#174).
|
|
25
|
+
* - No interpolation (§5, B3): the installed system-prompt has no
|
|
26
|
+
* `interpolate:false`; it interpolates `{{name}}` groups and fails on an
|
|
27
|
+
* unknown one. Each section's text is therefore only `{{a2a_node_*}}`,
|
|
28
|
+
* and the frozen text rides a scoped prompt VARIABLE — substituted values
|
|
29
|
+
* are never scanned again, so a `{{x}}` written into a layer stays literal
|
|
30
|
+
* and assembly cannot fail on it.
|
|
31
|
+
* - Never a veto (§3): mounting runs inside `agent/created`, where a throw
|
|
32
|
+
* would block the session from publishing. Every mount path catches and
|
|
33
|
+
* logs; a corrupt roster, a missing node or a tampered version file
|
|
34
|
+
* leaves the session without node sections, never without a session.
|
|
35
|
+
* - The task context `a2a:node:tasks` (N6) is LIVE, not frozen: it is a
|
|
36
|
+
* runtime context that the system prompt materialises per assembly, so a
|
|
37
|
+
* pointer that changes state shows at the next step (§5 "即时"). Its text
|
|
38
|
+
* rides a scoped variable too. A pointer that finished calls `remind`:
|
|
39
|
+
* each live session carrying that node gets a follow-up turn (§6).
|
|
40
|
+
* - `node_update_self` (N4) is registered with the sections on the bound
|
|
41
|
+
* session's own scope, and leaves with them. A write it makes is echoed in
|
|
42
|
+
* a live `a2a:node:self` context for the rest of that session (the frozen
|
|
43
|
+
* sections do not change, §5).
|
|
44
|
+
* @module @nelsonlongxiang/dsh-open-a2a-net/node-bind
|
|
45
|
+
*/
|
|
46
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
47
|
+
import { type UserMessage } from '@deepseek-ai/dsh-llm';
|
|
48
|
+
import { type NodeRecord, type NodeResult, type NodeStore, type PromptVersion } from './node-store.ts';
|
|
49
|
+
import { type SelfUpdateDeps } from './node-self.ts';
|
|
50
|
+
/** §5: section names, orders and the scoped variables that carry their frozen text. */
|
|
51
|
+
export declare const NODE_PROMPT: {
|
|
52
|
+
readonly mutable: {
|
|
53
|
+
readonly section: "a2a:node:mutable";
|
|
54
|
+
readonly order: 10150;
|
|
55
|
+
readonly variable: "a2a_node_mutable";
|
|
56
|
+
};
|
|
57
|
+
readonly core: {
|
|
58
|
+
readonly section: "a2a:node:core";
|
|
59
|
+
readonly order: 10180;
|
|
60
|
+
readonly variable: "a2a_node_core";
|
|
61
|
+
};
|
|
62
|
+
readonly tasks: {
|
|
63
|
+
readonly context: "a2a:node:tasks";
|
|
64
|
+
readonly order: 9150;
|
|
65
|
+
readonly variable: "a2a_node_tasks";
|
|
66
|
+
};
|
|
67
|
+
readonly self: {
|
|
68
|
+
readonly context: "a2a:node:self";
|
|
69
|
+
readonly order: 9160;
|
|
70
|
+
readonly variable: "a2a_node_self";
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export interface NodeBinding {
|
|
74
|
+
readonly session: string;
|
|
75
|
+
readonly node: string;
|
|
76
|
+
readonly by: string;
|
|
77
|
+
readonly at: string;
|
|
78
|
+
/**
|
|
79
|
+
* What the session's last mount froze, per layer: version (0 = the layer had none, so it mounts
|
|
80
|
+
* empty) and that version's sha. Absent = never recorded (an older row): the current version.
|
|
81
|
+
* A plugin reload re-mounts at these (#174).
|
|
82
|
+
*/
|
|
83
|
+
readonly core_version?: number;
|
|
84
|
+
readonly core_sha256?: string;
|
|
85
|
+
readonly mutable_version?: number;
|
|
86
|
+
readonly mutable_sha256?: string;
|
|
87
|
+
}
|
|
88
|
+
/** One layer's frozen pin as recorded on a row, or undefined when the row never recorded it. */
|
|
89
|
+
export type LayerPin = {
|
|
90
|
+
readonly version: number;
|
|
91
|
+
readonly sha256: string;
|
|
92
|
+
} | undefined;
|
|
93
|
+
/**
|
|
94
|
+
* A code fence left open would swallow everything rendered after it (even the core layer), so one
|
|
95
|
+
* still open at the end is closed. Tracked by the CommonMark rules: an opener is ``` or ~~~ (three
|
|
96
|
+
* or more) indented at most three spaces, and a backtick opener's info string has no backtick (else
|
|
97
|
+
* it is inline code); inside, only a run of the SAME character, at least as long, followed by nothing
|
|
98
|
+
* but blanks closes it. Lines split on CRLF, CR or LF alike; the closer carries the opener's indent, so
|
|
99
|
+
* a fence opened inside a list item is closed inside it rather than opening a new one at column 0.
|
|
100
|
+
*/
|
|
101
|
+
export declare function closeFences(text: string): string;
|
|
102
|
+
export declare function fenceSafe(text: string): string;
|
|
103
|
+
/** The mutable layer, fenced and marked lower than the core layer below it. '' when every section is empty. */
|
|
104
|
+
export declare function renderMutable(v: PromptVersion | undefined): string;
|
|
105
|
+
/** The core layer: identity from the record, the human-written text, and the precedence line. */
|
|
106
|
+
export declare function renderCore(node: NodeRecord, v: PromptVersion | undefined): string;
|
|
107
|
+
/**
|
|
108
|
+
* The binding table. A file that does not parse is left in place and the
|
|
109
|
+
* table comes back READ-ONLY and empty (sessions open, nothing mounts);
|
|
110
|
+
* a copy is kept beside it, as the node roster does.
|
|
111
|
+
*/
|
|
112
|
+
export declare class NodeBindings {
|
|
113
|
+
private readonly root;
|
|
114
|
+
private readonly now;
|
|
115
|
+
private rows;
|
|
116
|
+
private readOnlyReason;
|
|
117
|
+
/** @param root - the `<home>/a2a` directory; '' keeps the table in memory (tests). */
|
|
118
|
+
constructor(root: string, now?: () => string);
|
|
119
|
+
private get file();
|
|
120
|
+
private restore;
|
|
121
|
+
private commit;
|
|
122
|
+
get readOnly(): string;
|
|
123
|
+
list(): readonly NodeBinding[];
|
|
124
|
+
get(session: string): NodeBinding | undefined;
|
|
125
|
+
set(session: string, node: string, by: string): NodeResult<NodeBinding>;
|
|
126
|
+
/** Record what a mount froze (best effort; unchanged values write nothing). */
|
|
127
|
+
setFrozen(session: string, core: {
|
|
128
|
+
version: number;
|
|
129
|
+
sha256: string;
|
|
130
|
+
}, mutable: {
|
|
131
|
+
version: number;
|
|
132
|
+
sha256: string;
|
|
133
|
+
}): NodeResult<true>;
|
|
134
|
+
delete(session: string): NodeResult<true>;
|
|
135
|
+
/** Delete every row matching `drop` in one write; the deleted rows on success. */
|
|
136
|
+
deleteWhere(drop: (b: NodeBinding) => boolean): NodeResult<NodeBinding[]>;
|
|
137
|
+
}
|
|
138
|
+
/** The part of a live session the binder touches: its id and its own registration scope. */
|
|
139
|
+
export interface BindableAgent {
|
|
140
|
+
readonly id: unknown;
|
|
141
|
+
readonly ctx: Context;
|
|
142
|
+
/** Queue a follow-up turn (the §6 reminder); absent on test doubles that never get one. */
|
|
143
|
+
followup?(message: UserMessage): void;
|
|
144
|
+
/** Queue a message for the next turn without waking (an owed reminder handed out at mount). */
|
|
145
|
+
send?(message: UserMessage, target: 'next-turn' | 'next-step', wakeup: boolean): void;
|
|
146
|
+
}
|
|
147
|
+
export interface MountedNode {
|
|
148
|
+
readonly node: string;
|
|
149
|
+
readonly core_version: number;
|
|
150
|
+
readonly mutable_version: number;
|
|
151
|
+
}
|
|
152
|
+
export interface NodeBinderDeps {
|
|
153
|
+
readonly store: NodeStore;
|
|
154
|
+
readonly bindings: NodeBindings;
|
|
155
|
+
readonly logger: {
|
|
156
|
+
warn: (format: string, ...args: unknown[]) => void;
|
|
157
|
+
};
|
|
158
|
+
/** Whether a session is joined (§3: only a joined session binds). */
|
|
159
|
+
readonly joined: (session: string) => boolean;
|
|
160
|
+
/** Every joined session now (reconcile works on the difference from the last reading). */
|
|
161
|
+
readonly joinedList: () => readonly string[];
|
|
162
|
+
/** Whether a session is archived (it never mounts and cannot bind). */
|
|
163
|
+
readonly archived: (session: string) => boolean;
|
|
164
|
+
/** The live top-level session with this id, if any. */
|
|
165
|
+
readonly live: (session: string) => BindableAgent | undefined;
|
|
166
|
+
/** The node's task context text, read at each assembly ('' = none). */
|
|
167
|
+
readonly tasksText?: (node: string) => string;
|
|
168
|
+
/** Reminders owed to the node (finished while no session carried it); taken — and cleared — at mount. */
|
|
169
|
+
readonly pendingReminders?: (node: string) => string[];
|
|
170
|
+
/** N4: what `node_update_self` needs (absent = no tool). */
|
|
171
|
+
readonly selfUpdate?: Omit<SelfUpdateDeps, 'onWritten'>;
|
|
172
|
+
}
|
|
173
|
+
export interface NodeBinder {
|
|
174
|
+
/**
|
|
175
|
+
* Mount the session's bound node, if any. Never throws (it runs inside agent/created). A new session
|
|
176
|
+
* (agent/created) takes the current versions; `atFrozen` — the plugin-load pass over sessions that are
|
|
177
|
+
* already live — re-mounts at the versions the row recorded, so a plugin reload never changes a live
|
|
178
|
+
* session's head (§5; #174). A recorded version that no longer reads back falls back to the current one.
|
|
179
|
+
*/
|
|
180
|
+
mount(agent: BindableAgent, opts?: {
|
|
181
|
+
atFrozen?: boolean;
|
|
182
|
+
}): void;
|
|
183
|
+
/** Take a live session's node sections off, keeping its binding row (an archived session). */
|
|
184
|
+
suspend(session: string): void;
|
|
185
|
+
/** Forget a session's mount without touching its binding (cooling unload; the scope disposes itself). */
|
|
186
|
+
forget(session: string): void;
|
|
187
|
+
/** What a live session currently carries (the frozen snapshot), if anything. */
|
|
188
|
+
mounted(session: string): MountedNode | undefined;
|
|
189
|
+
/**
|
|
190
|
+
* #174: whether the session's assembled system prompt still carries the two node sections. A
|
|
191
|
+
* `complete` preset replaces the whole prompt and drops them though they stay mounted. Assembles
|
|
192
|
+
* with the session's own scope, as the agent loop does; undefined when the session carries no node
|
|
193
|
+
* or the assembly fails.
|
|
194
|
+
*/
|
|
195
|
+
assembled(session: string): Promise<{
|
|
196
|
+
core: boolean;
|
|
197
|
+
mutable: boolean;
|
|
198
|
+
} | undefined>;
|
|
199
|
+
bind(session: string, node: string, by: string): NodeResult<NodeBinding & {
|
|
200
|
+
mounted: boolean;
|
|
201
|
+
}>;
|
|
202
|
+
unbind(session: string): NodeResult<true>;
|
|
203
|
+
/**
|
|
204
|
+
* End the bindings of sessions that LEFT since the last reading (leave, JOIN_CAP eviction). Only
|
|
205
|
+
* the difference counts: a joined.json that failed to parse reads as empty, and treating every
|
|
206
|
+
* session as "left" on that reading would end every binding (the 3081 lesson). A failed write keeps
|
|
207
|
+
* the leavers for the next reading (retry). A session that (re)joined and carries a binding row
|
|
208
|
+
* mounts at once, if live. Never throws.
|
|
209
|
+
*/
|
|
210
|
+
reconcile(): void;
|
|
211
|
+
/** End every binding to an archived node. */
|
|
212
|
+
nodeArchived(node: string): void;
|
|
213
|
+
/** Dispose every mount this binder made (plugin unload). */
|
|
214
|
+
disposeAll(): void;
|
|
215
|
+
/** Queue a follow-up turn on every live, mounted session carrying `node` (§6 reminder); how many got it. */
|
|
216
|
+
remind(node: string, text: string): number;
|
|
217
|
+
}
|
|
218
|
+
/** One §6 reminder as a user message; the sender label makes ui-chat render it as "来自 …" with the text. */
|
|
219
|
+
export declare function reminderMessage(node: string, text: string): UserMessage;
|
|
220
|
+
export declare function createNodeBinder(d: NodeBinderDeps): NodeBinder;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub check for node task pointers (docs/design/node-identity.md §6, §8-8; slice N6).
|
|
3
|
+
*
|
|
4
|
+
* - Credentials are the host's own: `GITHUB_TOKEN` in the environment, else
|
|
5
|
+
* an existing `gh` login. The plugin stores none.
|
|
6
|
+
* - One call per pointer: the issues endpoint answers for issues AND pull
|
|
7
|
+
* requests (`pull_request.merged_at` says whether a PR merged).
|
|
8
|
+
* - Anything that is not a clean answer — no credential, offline, rate
|
|
9
|
+
* limited, a malformed body — is `undefined`: the state is UNKNOWN, never
|
|
10
|
+
* guessed, and an unknown state never triggers a reminder.
|
|
11
|
+
* @module @nelsonlongxiang/dsh-open-a2a-net/node-github
|
|
12
|
+
*/
|
|
13
|
+
export type TaskState = 'open' | 'merged' | 'closed' | 'declined';
|
|
14
|
+
/** `owner/repo#n` — the pointer form (GitHub's own owner and repo character sets; `.` and `..` are no repo). */
|
|
15
|
+
export declare const TASK_REF: RegExp;
|
|
16
|
+
/** One pointer's state on GitHub, and when it finished (merged_at / closed_at), if it did. */
|
|
17
|
+
export interface TaskFact {
|
|
18
|
+
readonly state: TaskState;
|
|
19
|
+
readonly finishedAt?: string;
|
|
20
|
+
}
|
|
21
|
+
/** One pointer's fact on GitHub, or undefined when it cannot be known right now. */
|
|
22
|
+
export type GithubProbe = (ref: string) => Promise<TaskFact | undefined>;
|
|
23
|
+
/** Map one issues-API body to a fact; undefined for anything that does not look like one. */
|
|
24
|
+
export declare function factOf(body: unknown): TaskFact | undefined;
|
|
25
|
+
/** Just the state of {@link factOf}. */
|
|
26
|
+
export declare function stateOf(body: unknown): TaskState | undefined;
|
|
27
|
+
export interface GithubProbeDeps {
|
|
28
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
29
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
30
|
+
/** Runs `gh api <path>` and resolves its stdout; rejects when gh is missing, logged out or fails. */
|
|
31
|
+
readonly gh?: (path: string) => Promise<string>;
|
|
32
|
+
readonly timeoutMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** The production probe: token first, then `gh`, else unknown. Never throws. */
|
|
35
|
+
export declare function githubProbe(d?: GithubProbeDeps): GithubProbe;
|