@nanmicoder/dsh-agent-teams 0.1.22-rc.1 → 0.1.23
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 +35 -12
- package/README_ZH.md +35 -12
- package/compatibility.json +6 -10
- package/lib/artifacts.js +342 -0
- package/lib/capabilities.js +6 -4
- package/lib/client/ActivityPanel.js +5 -2
- package/lib/client/agent-teams-card-definition.js +100 -6
- package/lib/client.js +90 -7
- package/lib/client.js.map +1 -1
- package/lib/command.js +3 -2
- package/lib/git-artifact-stamp.json +2 -2
- package/lib/harness-compat.js +70 -21
- package/lib/index.js +26 -3
- package/lib/lineage.js +198 -0
- package/lib/mailbox.js +59 -34
- package/lib/members.js +301 -53
- package/lib/profiles.js +3 -2
- package/lib/quality-gates.js +850 -65
- package/lib/scheduler.js +22 -3
- package/lib/snapshot.js +22 -1
- package/lib/state.js +279 -13
- package/lib/tool-policies.js +187 -0
- package/lib/tools.js +370 -89
- package/lib/types/artifacts.d.ts +66 -0
- package/lib/types/client/activity-monitor.d.ts +6 -0
- package/lib/types/client/agent-teams-card-definition.d.ts +56 -0
- package/lib/types/harness-compat.d.ts +56 -14
- package/lib/types/index.d.ts +26 -1
- package/lib/types/lineage.d.ts +92 -0
- package/lib/types/members.d.ts +112 -1
- package/lib/types/profiles.d.ts +6 -0
- package/lib/types/quality-gates.d.ts +216 -10
- package/lib/types/scheduler.d.ts +2 -1
- package/lib/types/snapshot.d.ts +20 -0
- package/lib/types/state.d.ts +22 -5
- package/lib/types/tool-policies.d.ts +119 -0
- package/lib/types/tools.d.ts +12 -1
- package/lib/types/types.d.ts +95 -0
- package/lib/types/workspace-audit.d.ts +27 -0
- package/lib/workspace-audit.js +52 -0
- package/package.json +59 -52
- package/pnpm-workspace.yaml +273 -273
- package/release-notes/v0.1.22.md +18 -0
- package/release-notes/v0.1.23.md +34 -0
- package/scripts/doctor.mjs +4 -4
- package/scripts/legacy-session-source-repair.mjs +344 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace byte identity: the only module in this plugin that reads artifact
|
|
3
|
+
* bytes off disk.
|
|
4
|
+
*
|
|
5
|
+
* Two plugin surfaces need it:
|
|
6
|
+
* · a quality verdict must record which revision it judged (issue #161), so
|
|
7
|
+
* delivery can refuse to advertise bytes no review ever saw;
|
|
8
|
+
* · an auto-generated work order must record the workspace it was derived
|
|
9
|
+
* from (issue #174), so it can be marked stale after a later round edits a
|
|
10
|
+
* cited file.
|
|
11
|
+
*
|
|
12
|
+
* Safety rules this module must never lose:
|
|
13
|
+
* · directory traversal never follows symlinks, and every explicitly named
|
|
14
|
+
* path is resolved with `realpath` and rejected when it leaves the
|
|
15
|
+
* workspace — a symlink must not become a read of arbitrary host files, and
|
|
16
|
+
* a symlink cycle must not become an infinite walk;
|
|
17
|
+
* · a scope that cannot be enumerated completely (file cap, unreadable
|
|
18
|
+
* directory, non-workspace-relative path) produces an explicit
|
|
19
|
+
* {@link ArtifactScopeGap} instead of a short list that reads as complete;
|
|
20
|
+
* · "could not be read" is recorded separately from "does not exist";
|
|
21
|
+
* · plugins and secrets never enter a broad scope by accident.
|
|
22
|
+
*
|
|
23
|
+
* The gate rules themselves stay pure: they receive {@link ArtifactDigest}
|
|
24
|
+
* values from here and never touch the filesystem.
|
|
25
|
+
* @module dsh-agent-teams/artifacts
|
|
26
|
+
*/
|
|
27
|
+
import type { ArtifactDigest, ArtifactScopeGap, ArtifactSnapshot } from './types.ts';
|
|
28
|
+
/**
|
|
29
|
+
* Upper bound on files expanded from one scope. Verdicts are about a revision
|
|
30
|
+
* and an unbounded walk of a large tree would make every gate close slow, so
|
|
31
|
+
* the walk stops here — and says so with a gap, because a silently truncated
|
|
32
|
+
* list would let a change outside the list pass as "unchanged".
|
|
33
|
+
*/
|
|
34
|
+
export declare const MAX_EXPANDED_ARTIFACTS = 200;
|
|
35
|
+
/** Result of reading a declared scope: what was read, and what could not be. */
|
|
36
|
+
export interface ArtifactScopeRead {
|
|
37
|
+
artifacts: ArtifactDigest[];
|
|
38
|
+
gaps: ArtifactScopeGap[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Read a declared scope: expand directory patterns to their current files,
|
|
42
|
+
* keep explicitly named paths even when they do not exist (a later creation is
|
|
43
|
+
* drift too), and report every part that could not be fully resolved.
|
|
44
|
+
*/
|
|
45
|
+
export declare function readArtifactScope(workspace: string, patterns: readonly string[]): ArtifactScopeRead;
|
|
46
|
+
/**
|
|
47
|
+
* Read the current bytes of workspace-relative paths (directories expand).
|
|
48
|
+
* Illegal and unreadable paths are reported, never silently dropped.
|
|
49
|
+
*/
|
|
50
|
+
export declare function digestWorkspacePaths(workspace: string, paths: readonly string[]): ArtifactDigest[];
|
|
51
|
+
/**
|
|
52
|
+
* Does this workspace-relative path currently exist as a file or directory?
|
|
53
|
+
* Uses the same containment rules as the digest reader: a symlink that leaves
|
|
54
|
+
* the workspace is not "an existing workspace path".
|
|
55
|
+
*/
|
|
56
|
+
export declare function workspacePathExists(workspace: string, path: string): boolean;
|
|
57
|
+
/** Expand scope patterns to the concrete files they currently cover. */
|
|
58
|
+
export declare function expandScopePaths(workspace: string, patterns: readonly string[]): string[];
|
|
59
|
+
/**
|
|
60
|
+
* Read one coherent snapshot of the given scope patterns. Directory patterns
|
|
61
|
+
* expand to the files they currently contain; a pattern naming a path that
|
|
62
|
+
* does not exist is recorded as `present: false` so a later creation or
|
|
63
|
+
* deletion is visible as drift, and any incomplete coverage is carried in
|
|
64
|
+
* `gaps`.
|
|
65
|
+
*/
|
|
66
|
+
export declare function snapshotWorkspacePaths(workspace: string, patterns: readonly string[]): ArtifactSnapshot;
|
|
@@ -30,6 +30,12 @@ export interface ActivityTask {
|
|
|
30
30
|
readonly kind?: string;
|
|
31
31
|
readonly round?: number;
|
|
32
32
|
readonly verdict?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Server-side audit of this task's verdict against the current bytes. Set
|
|
35
|
+
* when the recorded pass (or its generated work order) no longer describes
|
|
36
|
+
* the workspace, so the panel cannot render a bare green verdict.
|
|
37
|
+
*/
|
|
38
|
+
readonly verdictStale?: string;
|
|
33
39
|
/** Durable last-write stamp; drives the finished-member ordering (issue #192). */
|
|
34
40
|
readonly updatedAt?: number;
|
|
35
41
|
}
|
|
@@ -30,19 +30,75 @@ declare module '@deepseek-ai/dsh-client-ui-chat/client' {
|
|
|
30
30
|
'agent-teams': AgentTeamsCardData;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
/** One roster row folded from the create result. */
|
|
34
|
+
export interface AgentTeamsCardMember {
|
|
35
|
+
readonly id: string;
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly role: string;
|
|
38
|
+
}
|
|
33
39
|
/** Folded team record (the node's business state). */
|
|
34
40
|
export interface AgentTeamsNodeState {
|
|
35
41
|
readonly teamId: string;
|
|
36
42
|
readonly name: string;
|
|
37
43
|
readonly accepted: boolean;
|
|
44
|
+
/** Captain session the host reported, or `''` for a pre-#203 record. */
|
|
45
|
+
readonly captainSessionId: string;
|
|
46
|
+
/** Roster the host reported, or `[]` for a pre-#203 record. */
|
|
47
|
+
readonly members: readonly AgentTeamsCardMember[];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Canonical identity the host publishes on the create result (issue #203).
|
|
51
|
+
* The display name intentionally stays the create call's own name.
|
|
52
|
+
*/
|
|
53
|
+
export interface AgentTeamsCreateIdentity {
|
|
54
|
+
readonly teamId: string;
|
|
55
|
+
readonly captainSessionId: string;
|
|
56
|
+
readonly members: readonly AgentTeamsCardMember[];
|
|
38
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Fold a team name the way the host's `sanitizeKey` does, minus the host's
|
|
60
|
+
* digest fallback. The host keeps Unicode letters and digits, so an ASCII-only
|
|
61
|
+
* client derivation turned every CJK name into the shared `team` key and the
|
|
62
|
+
* card could never match its live team (issue #203). Names with no letters or
|
|
63
|
+
* digits at all, and names longer than the host's key limit, still need the
|
|
64
|
+
* host's canonical id; the create result now carries it.
|
|
65
|
+
* @param name - the create call's team name.
|
|
66
|
+
* @returns the host-shaped key for the name.
|
|
67
|
+
*/
|
|
68
|
+
export declare function teamKeyFromName(name: string): string;
|
|
39
69
|
/** Parse the only create-call fields the historic card owns. */
|
|
40
70
|
export declare function parseAgentTeamsCreateArgs(value: string): {
|
|
41
71
|
teamId: string;
|
|
42
72
|
name: string;
|
|
43
73
|
} | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Read the canonical identity the host attaches to the `agent_teams_create`
|
|
76
|
+
* result. Records written before the host published it carry no metadata, so
|
|
77
|
+
* an absent or malformed payload falls back to the client derivation.
|
|
78
|
+
* @param meta - the durable `tool/result` metadata payload, if any.
|
|
79
|
+
* @returns the canonical identity, or `undefined` when the host published none.
|
|
80
|
+
*/
|
|
81
|
+
export declare function parseAgentTeamsCreateResult(meta: unknown): AgentTeamsCreateIdentity | undefined;
|
|
82
|
+
/**
|
|
83
|
+
* Read the canonical team id out of a `tool/result` recorded before the host
|
|
84
|
+
* published {@link parseAgentTeamsCreateResult}'s metadata. Both historic
|
|
85
|
+
* renderings carry it and have been stable since they were introduced:
|
|
86
|
+
* `Team "<name>" created (id <canonical>) under <state_dir>. You are the
|
|
87
|
+
* captain.` and the staged `Team "<name>" plan created under <state_dir>. It is
|
|
88
|
+
* staged: …`, whose `state_dir` ends in the canonical id. This is the only
|
|
89
|
+
* legacy path for names the client cannot re-derive (host digest fallback,
|
|
90
|
+
* over-long names). Matches are taken right-to-left because the team name sits
|
|
91
|
+
* in front of the canonical segment and may itself contain a look-alike.
|
|
92
|
+
* @param text - the concatenated text blocks of one successful create result.
|
|
93
|
+
* @returns the canonical id, or `undefined` when the text carries none.
|
|
94
|
+
*/
|
|
95
|
+
export declare function teamIdFromCreateResultText(text: string): string | undefined;
|
|
44
96
|
/** Durable first-party tool events folded into one keyed Chat node. */
|
|
45
97
|
export declare const agentTeamsCardDefinition: ConversationNodeDefinition<AgentTeamsNodeState>;
|
|
98
|
+
/** Concatenate the text blocks of one durable tool-result message. */
|
|
99
|
+
export declare function resultText(message: {
|
|
100
|
+
readonly content: readonly unknown[];
|
|
101
|
+
}): string;
|
|
46
102
|
/** V4 tool-role results carry isError directly; older logs nest tool-result blocks. */
|
|
47
103
|
export declare function toolResultFailed(message: {
|
|
48
104
|
readonly content: readonly unknown[];
|
|
@@ -9,8 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { Context } from '@deepseek-ai/cordis';
|
|
11
11
|
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
12
|
-
import type { ContentBlock, MessageId } from '@deepseek-ai/dsh-llm';
|
|
12
|
+
import type { ContentBlock, MessageId, MessageSource } from '@deepseek-ai/dsh-llm';
|
|
13
13
|
import type { Session, SessionEvent, SessionId } from '@deepseek-ai/dsh-session';
|
|
14
|
+
import { type MemberToolFilter } from './tool-policies.ts';
|
|
14
15
|
type Setup = (childCtx: Context, child: Agent) => () => void;
|
|
15
16
|
declare module '@deepseek-ai/dsh-llm' {
|
|
16
17
|
interface MessageSourceMap {
|
|
@@ -19,6 +20,41 @@ declare module '@deepseek-ai/dsh-llm' {
|
|
|
19
20
|
};
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
/** Package identity a released plugin-wrapper source names on older hosts. */
|
|
24
|
+
export declare const AGENT_TEAMS_PLUGIN_NAME = "@nanmicoder/dsh-agent-teams";
|
|
25
|
+
/**
|
|
26
|
+
* First stored format generation whose migration edges admit producer-owned
|
|
27
|
+
* source kinds. The released v2→v3 payload audit validates every message source
|
|
28
|
+
* against a closed list of released kinds, so a stored format older than this
|
|
29
|
+
* must carry a released source or the whole session becomes unreadable once a
|
|
30
|
+
* newer host opens it (#160).
|
|
31
|
+
*/
|
|
32
|
+
export declare const RELEASED_SOURCE_VOCABULARY_VERSION = 3;
|
|
33
|
+
/** Source kinds this plugin mints on a host whose stored format admits them. */
|
|
34
|
+
export type AgentTeamsSource = {
|
|
35
|
+
readonly kind: 'agent-teams';
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: 'agent-teams-command';
|
|
38
|
+
readonly goal?: string;
|
|
39
|
+
readonly profile?: string;
|
|
40
|
+
};
|
|
41
|
+
/** Stored session format generation of the running host; 0 when it exposes none. */
|
|
42
|
+
export declare function hostSessionFormatVersion(): number;
|
|
43
|
+
/**
|
|
44
|
+
* Source for one AgentTeams-authored message on the running host.
|
|
45
|
+
*
|
|
46
|
+
* Current hosts mint producer-owned kinds: the Harness documents message
|
|
47
|
+
* sources as a merge-extensible vocabulary without a catch-all, so this plugin
|
|
48
|
+
* declares its own. Hosts that still store a pre-v3 generation are a different
|
|
49
|
+
* contract — their released v2→v3 migration rejects any source outside the
|
|
50
|
+
* released vocabulary, which is exactly how a plugin kind written on an older
|
|
51
|
+
* supported host poisons its session log for every later host. On those hosts
|
|
52
|
+
* the released plugin wrapper is used instead; the goal and profile already
|
|
53
|
+
* travel in the message text.
|
|
54
|
+
* @param modern - the producer-owned source to use where the host admits it.
|
|
55
|
+
* @returns a source accepted by the running host and by its migration chain.
|
|
56
|
+
*/
|
|
57
|
+
export declare function agentTeamsSource(modern: AgentTeamsSource): MessageSource;
|
|
22
58
|
/**
|
|
23
59
|
* 0.1.6 moved startup admission to serial agent/created. Older created events
|
|
24
60
|
* have no source and precede session-start; ignore those until the old hook.
|
|
@@ -54,29 +90,35 @@ export declare function restrictableToolNames(agent: Agent): ReadonlySet<string>
|
|
|
54
90
|
* forwarding a name the host dropped aborts every member spawn (#163, #164).
|
|
55
91
|
* Depth enforcement itself does not depend on these names —
|
|
56
92
|
* installMemberDelegationGuard bounds descendant creation by parent chain.
|
|
93
|
+
*
|
|
94
|
+
* This is the legacy filter of a member without a configured tool policy; a
|
|
95
|
+
* selected policy resolves through `resolveMemberToolFilter` instead (#59).
|
|
57
96
|
*/
|
|
58
|
-
export declare function memberToolFilter(maxDepth: number | undefined, knownTools: ReadonlySet<string> | undefined):
|
|
59
|
-
deny: string[];
|
|
60
|
-
};
|
|
97
|
+
export declare function memberToolFilter(maxDepth: number | undefined, knownTools: ReadonlySet<string> | undefined): MemberToolFilter;
|
|
61
98
|
/**
|
|
62
|
-
* Start one member, dropping
|
|
63
|
-
* retrying.
|
|
99
|
+
* Start one member, dropping only the built-in filter names the host reports
|
|
100
|
+
* as unknown and retrying.
|
|
64
101
|
*
|
|
65
102
|
* Resolving the filter against the registry view ({@link memberToolFilter})
|
|
66
103
|
* only covers hosts that expose one. A rejected filter still leaves the member
|
|
67
104
|
* without a session for the lifetime of the team, so the start must survive the
|
|
68
105
|
* cases that resolution cannot see — no registry view, a name that is known but
|
|
69
106
|
* not restrictable, a composition that mounted differently than expected. The
|
|
70
|
-
* host names the offenders in its rejection, so
|
|
71
|
-
*
|
|
72
|
-
*
|
|
107
|
+
* host names the offenders in its rejection, so the relaxable ones are removed
|
|
108
|
+
* and the start retried.
|
|
109
|
+
*
|
|
110
|
+
* Only `relaxableDeny` names may be dropped, and only from `deny`. The default
|
|
111
|
+
* is exactly the built-in captain-only and depth entries (#163/#164); a name an
|
|
112
|
+
* owner-declared tool policy contributed is never removed, because silently
|
|
113
|
+
* spawning a member with a different filter than the owner declared is contract
|
|
114
|
+
* drift, not recovery. Dropping a denied name the host does not register
|
|
115
|
+
* changes nothing; dropping an allowed name would loosen the policy, so `allow`
|
|
116
|
+
* is never relaxed and the four required coordination tools can never be
|
|
117
|
+
* removed.
|
|
73
118
|
* @param start - performs one start attempt for the given `toolFilter`.
|
|
74
119
|
* @param filter - the filter to attempt first.
|
|
120
|
+
* @param relaxableDeny - built-in deny names the retry may drop.
|
|
75
121
|
* @returns the started member.
|
|
76
122
|
*/
|
|
77
|
-
export declare function startMemberWithLenientFilter<T>(start: (filter:
|
|
78
|
-
deny: string[];
|
|
79
|
-
}) => Promise<T>, filter: {
|
|
80
|
-
deny: string[];
|
|
81
|
-
}): Promise<T>;
|
|
123
|
+
export declare function startMemberWithLenientFilter<T>(start: (filter: MemberToolFilter) => Promise<T>, filter: MemberToolFilter, relaxableDeny?: readonly string[]): Promise<T>;
|
|
82
124
|
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { Context } from '@deepseek-ai/cordis';
|
|
22
22
|
import z from '@deepseek-ai/schemastery';
|
|
23
23
|
import { type TeamProfileConfig } from './profiles.ts';
|
|
24
|
+
import { type MemberToolPolicy } from './tool-policies.ts';
|
|
24
25
|
export declare const name = "agent-teams";
|
|
25
26
|
export declare const inject: string[];
|
|
26
27
|
/** Plugin configuration. */
|
|
@@ -32,8 +33,22 @@ export interface Config {
|
|
|
32
33
|
stateDir?: string;
|
|
33
34
|
/** `ctx.subagents` provider used to spawn members; must support continuable children and personas (default `spawn`). */
|
|
34
35
|
memberProvider?: string;
|
|
35
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Optional model override applied to every member. The id is used verbatim.
|
|
38
|
+
* When the captain's provider does not advertise it, the provider that does
|
|
39
|
+
* is resolved from the live catalogs (only if exactly one advertises it);
|
|
40
|
+
* set `memberLlmProvider` to name the owner explicitly instead.
|
|
41
|
+
*/
|
|
36
42
|
memberModel?: string;
|
|
43
|
+
/**
|
|
44
|
+
* LLM provider route that owns `memberModel`. Set this when the member model
|
|
45
|
+
* is served by a different provider than the captain's session; without it
|
|
46
|
+
* the captain's provider is only used if it actually advertises the model,
|
|
47
|
+
* and an ambiguous or unowned model fails with the provider catalogs instead
|
|
48
|
+
* of being silently paired with the captain's route (issue #189).
|
|
49
|
+
* Distinct from `memberProvider`, which is the subagent runtime backend.
|
|
50
|
+
*/
|
|
51
|
+
memberLlmProvider?: string;
|
|
37
52
|
/** Prompt injected into member personas and automatic task assignments. */
|
|
38
53
|
executionPrompt?: string;
|
|
39
54
|
/** Plugin-wide fallback route for unavailable member models. */
|
|
@@ -44,6 +59,16 @@ export interface Config {
|
|
|
44
59
|
maxMembers?: number;
|
|
45
60
|
/** Named multi-role team profiles. */
|
|
46
61
|
profiles?: Record<string, TeamProfileConfig>;
|
|
62
|
+
/**
|
|
63
|
+
* Reusable per-member tool policies declared by the deployment owner (#59),
|
|
64
|
+
* keyed by policy id. A member selects one by id (`tool_policy` on
|
|
65
|
+
* `agent_teams_add_member`, a plan member, or a profile member row); the
|
|
66
|
+
* Captain model can select an existing policy but never submit raw lists.
|
|
67
|
+
* `allow` keeps only the named global tools (the four coordination tools are
|
|
68
|
+
* always added); `deny` removes the named global tools. Captain-only tools
|
|
69
|
+
* stay denied either way.
|
|
70
|
+
*/
|
|
71
|
+
toolPolicies?: Record<string, MemberToolPolicy>;
|
|
47
72
|
/** Prompt-section order for the usage policy (default `117`, after delegation policy). */
|
|
48
73
|
promptSectionOrder?: number;
|
|
49
74
|
/**
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable session lineage for team identity.
|
|
3
|
+
*
|
|
4
|
+
* Forking is a first-class DSH operation: the fork receives a new session id
|
|
5
|
+
* and records the source session as `header.parentSession`. Team identity must
|
|
6
|
+
* therefore be resolved along that chain, or a captain that continues in its
|
|
7
|
+
* own fork stops matching the team it leads (#176). Membership keeps its exact
|
|
8
|
+
* id match: a member is a subagent child of its captain, so its parent chain
|
|
9
|
+
* also contains the captain and must never be read as lead identity. Only a
|
|
10
|
+
* top-level conversation may inherit lead identity from an ancestor.
|
|
11
|
+
*
|
|
12
|
+
* Authorization paths resolve the chain durably: a hop is read from the live
|
|
13
|
+
* session when that session is resident and from the persistence service's
|
|
14
|
+
* stored header otherwise, so a fork still resolves its team while an
|
|
15
|
+
* intermediate session (or the original captain) is not loaded. The
|
|
16
|
+
* synchronous prompt-assembly path uses the calling session's own durable
|
|
17
|
+
* header plus resolvable live hops; failing to resolve there only leaves the
|
|
18
|
+
* default captain-side prompt in place — every tool re-authorizes
|
|
19
|
+
* asynchronously.
|
|
20
|
+
* @module dsh-agent-teams/lineage
|
|
21
|
+
*/
|
|
22
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
23
|
+
import type { Agent } from '@deepseek-ai/dsh-agent';
|
|
24
|
+
import type { Session } from '@deepseek-ai/dsh-session';
|
|
25
|
+
/** Maximum fork-chain depth resolved for one identity (cycle- and length-bounded). */
|
|
26
|
+
export declare const LINEAGE_MAX_DEPTH = 32;
|
|
27
|
+
/** One calling session's durable identity as the team lookups need it. */
|
|
28
|
+
export interface LeadIdentity {
|
|
29
|
+
/** The calling session id. */
|
|
30
|
+
readonly sessionId: string;
|
|
31
|
+
/** Ancestor session ids reachable through `header.parentSession`, nearest first. */
|
|
32
|
+
readonly ancestry: readonly string[];
|
|
33
|
+
/** Whether this session may inherit lead identity from an ancestor at all. */
|
|
34
|
+
readonly inheritsLead: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** The live session for one id: resident agent first, then the live session store. */
|
|
37
|
+
export declare function liveSession(ctx: Context, id: string): Session | undefined;
|
|
38
|
+
/** Whether one session is a top-level conversation rather than a subagent child. */
|
|
39
|
+
export declare function isTopLevelSession(session: Session): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Durable ancestry of one session, nearest first. The calling session's own
|
|
42
|
+
* header supplies the first hop; later hops come from resident sessions or
|
|
43
|
+
* stored headers, so a fork resolves through sessions that are not loaded.
|
|
44
|
+
* @param ctx - the plugin context (injects `agents`).
|
|
45
|
+
* @param session - the calling session.
|
|
46
|
+
* @returns ancestor session ids, bounded by {@link LINEAGE_MAX_DEPTH}.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveAncestry(ctx: Context, session: Session): Promise<readonly string[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Ancestry resolvable without awaiting: the calling session's durable first
|
|
51
|
+
* hop, then resident sessions, then whatever the async resolver cached.
|
|
52
|
+
*/
|
|
53
|
+
export declare function sessionAncestry(ctx: Context, session: Session): readonly string[];
|
|
54
|
+
/** Resolve one session's lead identity synchronously (prompt-assembly path). */
|
|
55
|
+
export declare function leadIdentityOf(ctx: Context, session: Session): LeadIdentity;
|
|
56
|
+
/** Resolve one session's lead identity durably (authorization paths). */
|
|
57
|
+
export declare function leadIdentityOfAsync(ctx: Context, session: Session): Promise<LeadIdentity>;
|
|
58
|
+
/** The lead-relevant view of a team record. */
|
|
59
|
+
export interface LeadScopedTeam {
|
|
60
|
+
readonly captainSessionId: string;
|
|
61
|
+
/** Present on real team state; omitting it keeps the pure exact-id contract. */
|
|
62
|
+
readonly members?: readonly {
|
|
63
|
+
readonly id: string;
|
|
64
|
+
readonly status?: string;
|
|
65
|
+
}[];
|
|
66
|
+
}
|
|
67
|
+
/** Whether one identity leads (or inherits the lead of) a team's recorded captain. */
|
|
68
|
+
export declare function teamLeadMatch(team: LeadScopedTeam, lead: LeadIdentity): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Whether one live session id leads a team: the recorded captain itself, or a
|
|
71
|
+
* top-level fork descendant of it. Members never match through their parent
|
|
72
|
+
* chain, and a member's fork does not match either.
|
|
73
|
+
*/
|
|
74
|
+
export declare function sessionLeadsTeam(ctx: Context, team: LeadScopedTeam, sessionId: string): boolean;
|
|
75
|
+
/** Durable {@link sessionLeadsTeam}: resolves the caller's chain through stored headers. */
|
|
76
|
+
export declare function sessionLeadsTeamAsync(ctx: Context, team: LeadScopedTeam, sessionId: string): Promise<boolean>;
|
|
77
|
+
/**
|
|
78
|
+
* Nearest live lead session of a team: the recorded captain while it is
|
|
79
|
+
* resident, otherwise the closest live fork descendant of it. Captain-directed
|
|
80
|
+
* reports use this so a human working in a fork still receives them, while the
|
|
81
|
+
* durable mail record stays addressed to the recorded captain.
|
|
82
|
+
* @param ctx - the plugin context (injects `agents`).
|
|
83
|
+
* @param team - current team state.
|
|
84
|
+
* @returns the live lead agent, or undefined when no lead session is resident.
|
|
85
|
+
*/
|
|
86
|
+
export declare function nearestLiveLead(ctx: Context, team: LeadScopedTeam, live?: readonly Agent[]): Agent | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Diagnostic note for a caller that matched no team but is a fork, so an
|
|
89
|
+
* operator can distinguish "no team exists" from "a team exists that this
|
|
90
|
+
* session does not lead".
|
|
91
|
+
*/
|
|
92
|
+
export declare function forkLineageNote(session: Session): string;
|
package/lib/types/members.d.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import type { Context } from '@deepseek-ai/cordis';
|
|
15
15
|
import { type Agent } from '@deepseek-ai/dsh-agent';
|
|
16
16
|
import type { Session } from '@deepseek-ai/dsh-session';
|
|
17
|
+
import type { MemberToolFilter } from './tool-policies.ts';
|
|
17
18
|
import { type TeamMember, type TeamState, type TeamTask } from './types.ts';
|
|
18
19
|
/** Persona snapshot of a profile protocol; the full text lives on team.json. */
|
|
19
20
|
export declare const PERSONA_PROTOCOL_MAX_CHARS = 400;
|
|
@@ -53,6 +54,8 @@ export interface MemberLlmSelectionRequest {
|
|
|
53
54
|
model?: string;
|
|
54
55
|
/** Plugin-level member model default. */
|
|
55
56
|
defaultModel?: string;
|
|
57
|
+
/** Configured member LLM provider, paired with `defaultModel` (issue #189). */
|
|
58
|
+
defaultProvider?: string;
|
|
56
59
|
/** Explicit reasoning effort; "default" selects the target model's default effort. */
|
|
57
60
|
reasoningEffort?: string;
|
|
58
61
|
/** Configured fallback route. */
|
|
@@ -61,6 +64,45 @@ export interface MemberLlmSelectionRequest {
|
|
|
61
64
|
model: string;
|
|
62
65
|
};
|
|
63
66
|
}
|
|
67
|
+
/** A composed member route plus where each half came from. */
|
|
68
|
+
export interface MemberRouteResolution {
|
|
69
|
+
readonly provider: {
|
|
70
|
+
readonly value: string;
|
|
71
|
+
readonly source: 'request' | 'config' | 'captain';
|
|
72
|
+
};
|
|
73
|
+
readonly model: {
|
|
74
|
+
readonly value: string;
|
|
75
|
+
readonly source: 'request' | 'config' | 'captain';
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Whether the provider half was *derived* rather than named. Only a
|
|
79
|
+
* model-only plugin default may have its owner looked up in the provider
|
|
80
|
+
* catalogs; a provider or model the caller named is never re-pointed.
|
|
81
|
+
*/
|
|
82
|
+
readonly derivedProvider: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Compose one member route from request, plugin configuration and the captain
|
|
86
|
+
* session, without performing any I/O. This is the single place where the
|
|
87
|
+
* precedence between the captain's route and a configured member default is
|
|
88
|
+
* decided, so a configured model can never be silently paired with whichever
|
|
89
|
+
* provider the captain happens to sit on (issue #189).
|
|
90
|
+
*
|
|
91
|
+
* Precedence: explicit request > plugin configuration > captain session. Model
|
|
92
|
+
* ids are used verbatim: a `/` inside a model id is part of that id and is
|
|
93
|
+
* never treated as a provider separator.
|
|
94
|
+
*
|
|
95
|
+
* `derivedProvider` is true only for a bare configured `memberModel` with no
|
|
96
|
+
* provider anywhere, which is the one shape whose owner must come from the
|
|
97
|
+
* provider catalogs.
|
|
98
|
+
* @param request - the captain-supplied and plugin-configured route halves.
|
|
99
|
+
* @param captain - the captain's current session route.
|
|
100
|
+
* @returns the composed route, or `undefined` when a half cannot be determined.
|
|
101
|
+
*/
|
|
102
|
+
export declare function composeMemberRoute(request: Pick<MemberLlmSelectionRequest, 'provider' | 'model' | 'defaultModel' | 'defaultProvider'>, captain: {
|
|
103
|
+
readonly provider: string;
|
|
104
|
+
readonly model: string;
|
|
105
|
+
}): MemberRouteResolution | undefined;
|
|
64
106
|
/** Process-local bridge between spawn admission and synchronous child setup. */
|
|
65
107
|
export interface MemberSelectionRuntime {
|
|
66
108
|
/** Trusted fresh-child admission, before the durable member id is written. */
|
|
@@ -98,6 +140,58 @@ interface FailedMemberAttempt {
|
|
|
98
140
|
readonly memberId: string;
|
|
99
141
|
readonly task?: Pick<TeamTask, 'id' | 'attempt' | 'attemptId'>;
|
|
100
142
|
}
|
|
143
|
+
/** How a final member turn failure relates to the durable record. */
|
|
144
|
+
export type MemberFailureDisposition =
|
|
145
|
+
/** A live observed attempt was failed and its task marked failed. */
|
|
146
|
+
'failed_attempt'
|
|
147
|
+
/** The work had already settled; only a durable captain note was added. */
|
|
148
|
+
| 'noted_settled'
|
|
149
|
+
/** No record: stale generation, superseded attempt, retired member, or no team. */
|
|
150
|
+
| 'ignored';
|
|
151
|
+
/**
|
|
152
|
+
* Classify a final member turn failure against the durable record. Pure, so the
|
|
153
|
+
* policy is testable without a lock or a Harness.
|
|
154
|
+
*
|
|
155
|
+
* A failure may only mutate the attempt the plugin actually observed. When that
|
|
156
|
+
* attempt is gone — the member moved on to newer work, the task was
|
|
157
|
+
* reassigned to a newer attempt, or the member generation was replaced — the
|
|
158
|
+
* late error carries no authority and stays silent. When the failure belongs to
|
|
159
|
+
* the current generation but its work already settled, the task and member are
|
|
160
|
+
* left exactly as they are and only a durable captain note is added: otherwise
|
|
161
|
+
* an operator cannot tell a clean run from one whose member's last turn died
|
|
162
|
+
* (429, auth failure, crash) after the delivery landed (issue #205).
|
|
163
|
+
* @param observed - the live member/task capability captured at the error event.
|
|
164
|
+
* @param current - the current durable member record and open task, if any.
|
|
165
|
+
* @returns the disposition for this failure.
|
|
166
|
+
*/
|
|
167
|
+
export declare function classifyMemberFailure(observed: FailedMemberAttempt, current: {
|
|
168
|
+
readonly member?: Pick<TeamMember, 'id' | 'status'> | undefined;
|
|
169
|
+
readonly task?: Pick<TeamTask, 'id' | 'attempt' | 'attemptId'> | undefined;
|
|
170
|
+
/** The durable record of the task the observation referred to, settled or not. */
|
|
171
|
+
readonly observedRecord?: Pick<TeamTask, 'id' | 'status' | 'subject'> | undefined;
|
|
172
|
+
}): MemberFailureDisposition;
|
|
173
|
+
/**
|
|
174
|
+
* The surviving durable record of the capability observed at the error event,
|
|
175
|
+
* or `undefined` when there is nothing honest to say about it.
|
|
176
|
+
*
|
|
177
|
+
* A task is only described as *settled* when its durable status is terminal
|
|
178
|
+
* (`completed`/`failed`/`cancelled`). A non-terminal record — for example a task
|
|
179
|
+
* that was claimed and then unclaimed or reassigned while the error was queued —
|
|
180
|
+
* must fall back to the plain "no open attempt" wording instead of implying a
|
|
181
|
+
* delivery landed.
|
|
182
|
+
*/
|
|
183
|
+
export declare function settledTaskRecord(observed: Pick<TeamTask, 'id'> | undefined, tasks: readonly Pick<TeamTask, 'id' | 'status' | 'subject'>[]): Pick<TeamTask, 'id' | 'status' | 'subject'> | undefined;
|
|
184
|
+
/**
|
|
185
|
+
* Build the durable captain note for one recorded failure. Pure, so the wording
|
|
186
|
+
* boundaries are testable without a lock or a Harness.
|
|
187
|
+
* @param memberName - the member that failed.
|
|
188
|
+
* @param summary - the failure message plus its code.
|
|
189
|
+
* @param disposition - what the failure was allowed to do.
|
|
190
|
+
* @param task - the open attempt that was failed, when there was one.
|
|
191
|
+
* @param settledRecord - the terminal record of the observed task, when it exists.
|
|
192
|
+
* @returns the exact message the captain will read.
|
|
193
|
+
*/
|
|
194
|
+
export declare function memberFailureMessage(memberName: string, summary: string, disposition: MemberFailureDisposition, task: Pick<TeamTask, 'id' | 'subject'> | undefined, settledRecord: Pick<TeamTask, 'id' | 'status' | 'subject'> | undefined): string;
|
|
101
195
|
/** Record a final turn failure, never an intermediate request retry. */
|
|
102
196
|
export declare function failMemberOpenAttempt(ctx: Context, stateRoot: string, teamId: string, memberName: string, failure: {
|
|
103
197
|
readonly code: string;
|
|
@@ -111,6 +205,15 @@ export declare function failMemberOpenAttempt(ctx: Context, stateRoot: string, t
|
|
|
111
205
|
* An explicit effort overrides either policy; the sentinel "default" also
|
|
112
206
|
* selects the target model's default. The final effort is validated against
|
|
113
207
|
* the target model before a child is created.
|
|
208
|
+
*
|
|
209
|
+
* A configured `memberModel` is a route preference, not a blind model-id
|
|
210
|
+
* override: it is paired with `memberLlmProvider` (or its own `provider/model`
|
|
211
|
+
* form) when configured, and when only a bare model id is configured it is
|
|
212
|
+
* adopted by the captain's provider only if that provider actually advertises
|
|
213
|
+
* it. Otherwise the catalog decides: exactly one advertising provider wins,
|
|
214
|
+
* none or several produce an actionable error listing them (issue #189). A
|
|
215
|
+
* provider the caller *named* — explicitly or via configuration — is never
|
|
216
|
+
* silently switched away from.
|
|
114
217
|
*/
|
|
115
218
|
export declare function resolveMemberLlmSelection(ctx: Context, captain: Agent, request: MemberLlmSelectionRequest, signal?: AbortSignal): Promise<MemberLlmSelection>;
|
|
116
219
|
/**
|
|
@@ -150,8 +253,16 @@ export declare function memberWelcome(team: TeamState, memberName: string): stri
|
|
|
150
253
|
* @param member - the member draft whose `id` is filled on success.
|
|
151
254
|
* @param stateDir - configured state directory (for the persona).
|
|
152
255
|
* @param signal - caller cancellation, forwarded to the start.
|
|
256
|
+
* @param initialPrompt - first turn text; the welcome message when omitted.
|
|
257
|
+
* @param resolvedFilter - the member's effective tool filter (a selected
|
|
258
|
+
* policy, or the legacy captain-only filter when the member has none). The
|
|
259
|
+
* caller resolves it so `spawnMember` forwards exactly one immutable filter
|
|
260
|
+
* to `startContinuable`.
|
|
261
|
+
* @param relaxableDeny - deny names the lenient retry may drop for this
|
|
262
|
+
* member; the caller narrows it for a policy member so a name the owner
|
|
263
|
+
* declared is never silently removed.
|
|
153
264
|
*/
|
|
154
|
-
export declare function spawnMember(ctx: Context, config: MemberRuntimeConfig, selections: MemberSelectionRuntime, llmSelection: MemberLlmSelection, captain: Agent, team: TeamState, member: TeamMember, stateDir: string, signal: AbortSignal, initialPrompt?: string): Promise<void>;
|
|
265
|
+
export declare function spawnMember(ctx: Context, config: MemberRuntimeConfig, selections: MemberSelectionRuntime, llmSelection: MemberLlmSelection, captain: Agent, team: TeamState, member: TeamMember, stateDir: string, signal: AbortSignal, initialPrompt?: string, resolvedFilter?: MemberToolFilter, relaxableDeny?: readonly string[]): Promise<void>;
|
|
155
266
|
/**
|
|
156
267
|
* Deliver one message to a member as its next FIFO turn. Best effort: a
|
|
157
268
|
* failure (member gone or not continuable) is logged and reported as `false`
|
package/lib/types/profiles.d.ts
CHANGED
|
@@ -27,6 +27,11 @@ export interface TeamProfileMemberConfig {
|
|
|
27
27
|
reasoning_effort?: string;
|
|
28
28
|
executionPrompt?: string;
|
|
29
29
|
fallback?: TeamModelFallbackConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Owner-declared tool-policy id this profile member selects (#59). The
|
|
32
|
+
* profile only names a configured policy; it never carries raw tool lists.
|
|
33
|
+
*/
|
|
34
|
+
tool_policy?: string;
|
|
30
35
|
}
|
|
31
36
|
/** One seed-task row in a named team-profile template (unresolved). */
|
|
32
37
|
export interface TeamProfileTaskConfig {
|
|
@@ -61,6 +66,7 @@ export interface NormalizedProfileMember {
|
|
|
61
66
|
reasoningEffort?: string;
|
|
62
67
|
executionPrompt?: string;
|
|
63
68
|
fallback?: TeamModelFallbackConfig;
|
|
69
|
+
toolPolicy?: string;
|
|
64
70
|
}
|
|
65
71
|
/** A profile seed task after assignee canonicalization; `sourceIndex` is the YAML order. */
|
|
66
72
|
export interface NormalizedProfileTask {
|