@aiwuk/team 5.0.3 → 5.0.8
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/CHANGELOG.md +19 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/index.js +2 -0
- package/member-models.js +84 -0
- package/memory-hub.js +9 -19
- package/memory-store.js +51 -0
- package/observability-projection.js +10 -35
- package/package.json +5 -3
- package/roles.js +38 -2
- package/teammate-context.js +1 -1
- package/theme-plugin.js +1 -1
- package/usage.js +4 -0
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,7 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
15
15
|
- Config pages: a secret row whose state cannot be read now shows "Could not check" with a Retry button instead of waiting forever; one automatic retry runs first.
|
|
16
16
|
- Optional macOS desktop Mothership restart adapter and installer: preserve the native connector protocol, terminate the complete desktop app gracefully, and relaunch once after an authenticated restart request. Includes lifecycle tests and deployment/rollback instructions.
|
|
17
17
|
|
|
18
|
+
- `@aiwuk/search`: opt-in native fetch provider `aiwuk-fetch`. It is enabled only when DSH's web setting `fetchProvider` or the environment variable `DSH_WEB_FETCH_PROVIDER` is `aiwuk-fetch`, and it is off by default to avoid `WEB_PROVIDER_AMBIGUOUS` with DSH's built-in `http` provider. `omni_fetch` is unchanged.
|
|
18
19
|
- New companion package `dsh-capability-autocomplete` 0.2.0 (`packages/autocomplete`): composer ghost-text suggestions for DSH Web, inline continuation while typing and next-message suggestions after a reply. Tab accepts, Alt+Up/Down cycles, Esc dismisses; nothing is sent automatically. Routes through the session model or a fixed provider/model.
|
|
19
20
|
- The owner-only `POST /autocomplete/predict` endpoint refuses cross-site requests, sends only user/assistant text as quoted context, caps concurrent predictions at four and never writes the transcript.
|
|
20
21
|
- Settings are read from the host ConfigForm (Settings → General and the plugin card) instead of browser storage, so the server and the client share one source of truth.
|
|
@@ -23,8 +24,26 @@ All notable changes to this project are documented in this file. The format foll
|
|
|
23
24
|
- Touch suggestion bar for phones and tablets: tappable suggestion buttons above the composer, a ✦ button to ask for a continuation, × to dismiss. Chosen by input type (`pointer: coarse`), switchable with `mobileBar`.
|
|
24
25
|
- `dsh-capability-autocomplete` 0.4.0: optional fleet defaults. A manager can push `AUTOCOMPLETE_PROVIDER`, `AUTOCOMPLETE_MODEL` (and optionally `AUTOCOMPLETE_ROUTE_MODE`) through the DSH credential store; they apply only while the user's own route is untouched, and the user's choice always wins as a whole.
|
|
25
26
|
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Node baseline raised to `>=24.9` for all packages, matching DSH 0.2.2-rc.1; the CI matrix runs Node 24.x.
|
|
30
|
+
- Generated files are no longer tracked in git: root and package `client.js` and `packages/*/shared/**` are built by `pretest`, by `prepack` (so `npm pack` and `npm publish` build them automatically) and by the CI "Build generated files" step. Parallel branches no longer conflict on them.
|
|
31
|
+
- Usage: the duplicate timing view is removed; wall times come from DSH's native `sessionStats`. The usage report state version moves from 3 to 4, and older checkpoints are discarded. The team's own token fold is kept, because native `tokenUsage` has no route dimension and counts fork-inherited prefix tokens.
|
|
32
|
+
- Memory: team `capability_memory_*` tools and `@aiwuk/memory` `memory_*` tools share one pipeline (`createMemoryCore` in `memory-store.js`). `@aiwuk/memory` gains a trusted `ingest()` and `markUsed` in search. Public `memory_write` still rejects role scope and `handoff`/`result` kinds. The storage format is unchanged.
|
|
33
|
+
- Design notes: `docs/design/p0-p2-native-reuse.md` records the native-reuse review. Native goals (same-session only), `workflowEngine` (would regress dispatch policy) and schedule/MCP/subagent (already native, no wrapper needed) were skipped.
|
|
34
|
+
|
|
35
|
+
### Released
|
|
36
|
+
|
|
37
|
+
- Team 5.0.8, Memory 1.0.5, Search 1.1.0 (Autocomplete stays at 1.0.4): Search 1.1.0 adds the opt-in native fetch provider `aiwuk-fetch` (off by default). Memory 1.0.5 and Team 5.0.8 share one write/search pipeline between the team hub and `@aiwuk/memory`, and Team drops the duplicated timing view in favour of the native `sessionStats`. Node >= 24.9 is now required, generated files (`client.js`, `shared/`) are built in `prepack`, and `release.yml` can publish `@aiwuk/autocomplete`.
|
|
38
|
+
|
|
26
39
|
### Fixed
|
|
27
40
|
|
|
41
|
+
- Composer placeholder overlap (Autocomplete 1.0.4): in next-message mode the ghost suggestion was drawn on top of DSH's native placeholder text. The placeholder is now hidden (via a scoped `data-aiwuk-ghost-next` attribute and a style rule, never by changing DSH text) while the next-mode ghost is visible and restored on dismiss, typing or unmount. The ghost also uses `white-space: pre` so its spacing is preserved.
|
|
42
|
+
- Autocomplete on desktop (Autocomplete 1.0.3): ghost text overlapped existing input text. It is now positioned right after the caret, using DOM-based caret-at-end detection that handles multi-paragraph (Lexical `<p>`) editors. It is hidden while the caret is not at the end and hides again on `selectionchange` and scroll. Prefix stripping is now case-insensitive.
|
|
43
|
+
- Roster model display for idle teammates (Team 5.0.7): once a teammate finished and DSH unloaded it, the roster fell back to its creation options again and showed the Lead model. The correction now also uses the teammate's last recorded `model/selection`, observed live or read once from its durable log through DSH's `sessionQuery` after a restart.
|
|
44
|
+
- Roster model display (Team 5.0.6): DSH's `list_agents` and `spawn_teammate` report a teammate's model from the options it was created with, the Lead model it inherited, so after role routing the Lead was told every teammate ran on the Lead's model. A `tools/post-execute` policy now rewrites the `model` of each roster row a capability-team member sees to that member's selected route (`agent.model.current()`); other presets, other tools and failed results are untouched.
|
|
45
|
+
- Teammates failed on their first step (Team 5.0.5): DSH 0.2.2-rc.1 fixes provider and model before step preparation and rejects an `agent/request` result naming another route, so per-role routing that switched a teammate off the inherited Lead model in `agent/request` ended the turn with "use agent.model.select() before step preparation". The role route is now selected with `agent.model.select()` when a teammate is created; a later route change (configuration edit, long-context switch, task override) is selected for the next step, and a same-tier failover switch ends the failed step and applies to the next one instead of retrying on the fixed route.
|
|
46
|
+
- Team collaboration (Memory 1.0.4, Team 5.0.4, Search 1.0.4): Team sessions created after a DSH restart had no `spawn_teammate`, `send_message`, `wait_agent` or `team_task_*` tools, so the Lead could plan a dispatch but never start teammates. DSH announces agents with `ctx.serial('agent/created')`, which stops at the first listener returning a value; the Memory listener returned its per-agent state and cut off the native Team tool installer behind it. Every `agent/created` listener now uses a block body and returns nothing, and `tests/agent-created-listeners.test.mjs` guards all packages against the pattern.
|
|
28
47
|
- Config pages (Team 5.0.3, Search 1.0.3, Memory 1.0.3, Autocomplete 1.0.2): a locked Mothership value now also drives dependent rows, so a Mothership-fixed autocomplete route shows its managed provider and model.
|
|
29
48
|
- Locked canonical managed credentials now override legacy/custom key aliases for their exact integration section and search engine. Missing locked credentials do not fall back to local aliases; standalone and unlocked configurations retain their existing behavior. Presence metadata reports the effective canonical ref, and both canonical credentials and ignored aliases remain covered by redaction. This shared resolver fix ships in Team 5.0.1, Search 1.0.1 and Memory 1.0.1.
|
|
30
49
|
- Config pages: secret rows could stay on "Checking..." although `credentials/describe` answered. Describe results are now ticketed (a stale or post-unmount result is ignored), a ref absent from a successful reply reads as not configured, a hung or failed call times out into the failure state, and the effect re-runs when the credentials service changes.
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/walke2019/dsh-capability-team/actions/workflows/ci.yml)
|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
**English** | [简体中文](README.zh-CN.md)
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@ A multi-agent team preset for DeepSeek Harness (DSH). It is a thin enhancement l
|
|
|
21
21
|
|
|
22
22
|
## Quick start
|
|
23
23
|
|
|
24
|
-
Requirements: DSH >= 0.2.2-rc.1 (below 0.3.0), Node.js >=
|
|
24
|
+
Requirements: DSH >= 0.2.2-rc.1 (below 0.3.0), Node.js >= 24.9, and the official `@deepseek-ai/dsh-experimental-agent-team-profile` bundle enabled in your DSH profile.
|
|
25
25
|
|
|
26
26
|
**1. Install**
|
|
27
27
|
|
package/README.zh-CN.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/walke2019/dsh-capability-team/actions/workflows/ci.yml)
|
|
6
6
|

|
|
7
7
|

|
|
8
|
-

|
|
9
9
|
|
|
10
10
|
[English](README.md) | **简体中文**
|
|
11
11
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
## 快速开始
|
|
23
23
|
|
|
24
|
-
前置条件:DSH >= 0.2.2-rc.1(且 < 0.3.0)、Node.js >=
|
|
24
|
+
前置条件:DSH >= 0.2.2-rc.1(且 < 0.3.0)、Node.js >= 24.9,并在 DSH profile 中启用官方 `@deepseek-ai/dsh-experimental-agent-team-profile` bundle。
|
|
25
25
|
|
|
26
26
|
**1. 安装**
|
|
27
27
|
|
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { apply as applyRoles } from './roles.js';
|
|
2
|
+
import { applyMemberModels } from './member-models.js';
|
|
2
3
|
import { ownsCapabilityTeam } from './preset-guard.js';
|
|
3
4
|
import { apply as applyIntegrations } from './integrations.js';
|
|
4
5
|
import { apply as applyTheme } from './theme-plugin.js';
|
|
@@ -81,6 +82,7 @@ export function apply(host, config = {}) {
|
|
|
81
82
|
applyAccess(ctx, team);
|
|
82
83
|
applyTeammateWait(ctx);
|
|
83
84
|
applyRoles(ctx, team, failover, observability, taskControl);
|
|
85
|
+
applyMemberModels(host);
|
|
84
86
|
applyIntegrations(ctx, team);
|
|
85
87
|
applyTheme(ctx, team, failover, workflow, planGate, taskControl);
|
|
86
88
|
}
|
package/member-models.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// DSH's native roster (list_agents, spawn_teammate) reports a teammate's model from the AgentOptions it was
|
|
2
|
+
// created with, which is the Lead's model it inherited. Role routing then records the real route with
|
|
3
|
+
// agent.model.select(), so the roster told the Lead every teammate ran on the Lead's model. This rewrites the
|
|
4
|
+
// roster rows a capability-team Lead sees to each live member's selected route; DSH stays the source of truth.
|
|
5
|
+
import { ownsCapabilityTeam } from './preset-guard.js';
|
|
6
|
+
import { restrictiveToolName } from './tool-names.js';
|
|
7
|
+
const ROSTER_TOOLS = new Set(['list_agents', 'spawn_teammate']);
|
|
8
|
+
/** The provider/model a live Agent currently runs (its recorded selection), or undefined. */
|
|
9
|
+
export function selectedRoute(agent) {
|
|
10
|
+
try {
|
|
11
|
+
const current = agent?.model?.current?.();
|
|
12
|
+
return current?.provider && current?.model ? { provider: current.provider, model: current.model } : undefined;
|
|
13
|
+
} catch { return undefined; }
|
|
14
|
+
}
|
|
15
|
+
/** The last `model/selection` recorded in a session log, or undefined. */
|
|
16
|
+
export function lastSelection(events) {
|
|
17
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
18
|
+
const { provider, model } = events[i]?.type === 'model/selection' ? events[i].data ?? {} : {};
|
|
19
|
+
if (typeof provider === 'string' && typeof model === 'string') return { provider, model };
|
|
20
|
+
}
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
/** One roster row with `model` replaced by the member's selected model id. The row's `provider` is the subagent backend (spawn/fork), not the LLM provider, so it is left as is. */
|
|
24
|
+
export function correctRow(row, lookup) {
|
|
25
|
+
if (!row || typeof row !== 'object' || typeof row.target !== 'string') return row;
|
|
26
|
+
const route = lookup(row.target);
|
|
27
|
+
if (!route || row.model === route.model) return row;
|
|
28
|
+
return { ...row, model: route.model };
|
|
29
|
+
}
|
|
30
|
+
/** Rewrite a native roster value (list_agents array or spawn_teammate `{ member }`); other values pass through. */
|
|
31
|
+
export function correctRoster(value, lookup) {
|
|
32
|
+
if (Array.isArray(value)) {
|
|
33
|
+
let changed = false;
|
|
34
|
+
const rows = value.map(row => { const next = correctRow(row, lookup); if (next !== row) changed = true; return next; });
|
|
35
|
+
return changed ? rows : value;
|
|
36
|
+
}
|
|
37
|
+
if (value && typeof value === 'object' && value.member) {
|
|
38
|
+
const member = correctRow(value.member, lookup);
|
|
39
|
+
return member === value.member ? value : { ...value, member };
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
export function applyMemberModels(ctx) {
|
|
44
|
+
// An idle teammate is unloaded; DSH then falls back to its creation options (the Lead model) and no live
|
|
45
|
+
// Agent is left to ask. Remember each session's last selection, which DSH records as `model/selection`.
|
|
46
|
+
const recorded = new Map(); // session id → { provider, model }
|
|
47
|
+
try {
|
|
48
|
+
ctx.on('session/event', (session, event) => {
|
|
49
|
+
if (event?.type !== 'model/selection' || !session?.id) return;
|
|
50
|
+
const { provider, model } = event.data ?? {};
|
|
51
|
+
if (typeof provider === 'string' && typeof model === 'string') recorded.set(session.id, { provider, model });
|
|
52
|
+
});
|
|
53
|
+
} catch { /* host without session observers: live selections only */ }
|
|
54
|
+
// After a DSH restart neither is available: read the member's durable log once (DSH's sessionQuery).
|
|
55
|
+
const coldRoute = async id => {
|
|
56
|
+
if (recorded.has(id)) return recorded.get(id);
|
|
57
|
+
try {
|
|
58
|
+
const snapshot = await ctx.get?.('sessionQuery')?.readSession(id);
|
|
59
|
+
const route = lastSelection(snapshot?.events ?? []);
|
|
60
|
+
if (route) recorded.set(id, route);
|
|
61
|
+
return route;
|
|
62
|
+
} catch { return undefined; }
|
|
63
|
+
};
|
|
64
|
+
const routesFor = async (lead, rows) => {
|
|
65
|
+
const members = ctx.agentTeams.listMembers(lead);
|
|
66
|
+
const routes = new Map([['lead', selectedRoute(lead)]]);
|
|
67
|
+
for (const row of Array.isArray(rows) ? rows : [rows?.member]) {
|
|
68
|
+
const member = members.find(m => m.name === row?.target && m.role !== 'lead');
|
|
69
|
+
if (member) routes.set(member.name, selectedRoute(ctx.agents?.get?.(member.id)) ?? await coldRoute(member.id));
|
|
70
|
+
}
|
|
71
|
+
return target => routes.get(target);
|
|
72
|
+
};
|
|
73
|
+
ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
74
|
+
const decision = await next();
|
|
75
|
+
try {
|
|
76
|
+
if (decision?.kind !== 'accept' || Object.hasOwn(decision, 'value') || Object.hasOwn(decision, 'content') || result?.isError) return decision;
|
|
77
|
+
if (!ROSTER_TOOLS.has(restrictiveToolName(exec?.name, ROSTER_TOOLS)) || !ownsCapabilityTeam(ctx, exec.agent)) return decision;
|
|
78
|
+
const membership = ctx.agentTeams.tryMembership(exec.agent);
|
|
79
|
+
if (!membership?.root) return decision;
|
|
80
|
+
const value = correctRoster(result.value, await routesFor(membership.root, result.value));
|
|
81
|
+
return value === result.value ? decision : { ...decision, value };
|
|
82
|
+
} catch { return decision; } // display correction must never break the native tool
|
|
83
|
+
});
|
|
84
|
+
}
|
package/memory-hub.js
CHANGED
|
@@ -6,13 +6,12 @@ import { m as modelText } from './i18n.js';
|
|
|
6
6
|
// context and does not resend the runtime snapshot every step.
|
|
7
7
|
import { createEmbedder } from './memory-embed.js';
|
|
8
8
|
import { createMemoryBackend } from './memory-backends.js';
|
|
9
|
-
import {
|
|
10
|
-
import { MEMORY_KINDS, MEMORY_LIMITS, createMemoryStore } from './memory-store.js';
|
|
9
|
+
import { MEMORY_KINDS, MEMORY_LIMITS, MEMORY_SCOPES, createMemoryCore, createMemoryStore } from './memory-store.js';
|
|
11
10
|
import { baseName } from './session-rotation.js';
|
|
12
11
|
import { params } from './tool-schema.js';
|
|
13
12
|
import { ownsCapabilityTeam } from './preset-guard.js';
|
|
14
13
|
const output = { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] };
|
|
15
|
-
const SCOPES =
|
|
14
|
+
const SCOPES = MEMORY_SCOPES;
|
|
16
15
|
const messageText = message => (Array.isArray(message?.content) ? message.content.map(c => c?.text ?? '').join('\n') : String(message?.text ?? message?.content ?? '')).trim();
|
|
17
16
|
export function createMemoryHub(ctx, team, { store = createMemoryStore(team.root), embedder = createEmbedder(), backend = createMemoryBackend({ label: modelText('memory-hub.280') }), secrets = () => [] } = {}) {
|
|
18
17
|
const recall = new Map(); // agent id → { query, block }
|
|
@@ -22,7 +21,9 @@ export function createMemoryHub(ctx, team, { store = createMemoryStore(team.root
|
|
|
22
21
|
if (!m || !ownsCapabilityTeam(ctx, agent)) return undefined;
|
|
23
22
|
return { project: team.pinned(agent) ?? team.pinned(m.root ?? agent), role: m.role === 'lead' ? 'lead' : baseName(m.name), session: agent.id, lead: m.role === 'lead', leadId: (m.root ?? agent).id, name: m.name ?? 'lead' };
|
|
24
23
|
};
|
|
25
|
-
|
|
24
|
+
// Redaction, embedding, scope mapping, write + shared-backend sync and use counting are shared with @aiwuk/memory.
|
|
25
|
+
const core = createMemoryCore({ store, embedder, backend, secrets });
|
|
26
|
+
const embedOne = (text, purpose) => core.embed(text, purpose);
|
|
26
27
|
async function backfill(project) {
|
|
27
28
|
if (!embedder.available) return;
|
|
28
29
|
const missing = await store.unembedded(project, 16, embedder.vectorKey);
|
|
@@ -36,24 +37,14 @@ export function createMemoryHub(ctx, team, { store = createMemoryStore(team.root
|
|
|
36
37
|
if (!SCOPES.includes(scope)) throw new Error(modelText('memory-hub.282', { 0: SCOPES.join(' / ') }));
|
|
37
38
|
if (scope === 'global' && !who.lead) throw new Error(modelText('memory-hub.283'));
|
|
38
39
|
// Secrets never enter memory: it is recalled into other members' prompts and may leave this machine.
|
|
39
|
-
const clean = redactSecrets(String(text ?? ''), await Promise.resolve(secrets()).catch(() => []));
|
|
40
|
-
const embedded = await embedOne(clean, 'passage');
|
|
41
|
-
const s = scope === 'global' ? { kind: 'global' } : scope === 'project' ? { kind: 'project', project: who.project }
|
|
42
|
-
: scope === 'session' ? { kind: 'session', project: who.project, session: who.session } : { kind: 'role', project: who.project, role: who.role };
|
|
43
|
-
const saved = await store.write({ scope: s, kind, text: clean, tags: Array.isArray(tags) ? tags.map(String).slice(0, 10) : [], key: key ? String(key).slice(0, 120) : null,
|
|
44
|
-
vector: embedded?.vector ?? null, model: embedded?.model ?? null, source: { member: who.name, session: who.session, lead: who.leadId, at: Date.now() } });
|
|
45
40
|
// Session-scoped notes stay local; everything else also becomes shared long-term memory.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (saved.superseded?.length) backend.revoke?.(saved.superseded);
|
|
49
|
-
if (!saved.merged || saved.superseded?.length) backend.enqueue(saved.memory, who);
|
|
50
|
-
} catch (error) { saved.syncError = error.message; }
|
|
51
|
-
}
|
|
41
|
+
const saved = await core.write(who, { kind, text, scope, project: who.project, key: key ? String(key).slice(0, 120) : null,
|
|
42
|
+
tags: Array.isArray(tags) ? tags.map(String).slice(0, 10) : [], source: { member: who.name, session: who.session, lead: who.leadId, at: Date.now() } });
|
|
52
43
|
return saved;
|
|
53
44
|
},
|
|
54
45
|
async search(who, { query, k = 6, kinds }) {
|
|
55
46
|
// The query goes to the embedding service and the shared backend, so it is filtered like a stored memory.
|
|
56
|
-
const q =
|
|
47
|
+
const q = (await core.clean(String(query ?? '').slice(0, 4000))).slice(0, 2000);
|
|
57
48
|
const limit = Math.min(Math.max(1, k), 20);
|
|
58
49
|
const [embedded, shared] = await Promise.all([q ? embedOne(q, 'query') : null, kinds ? [] : backend.recall(q)]);
|
|
59
50
|
void backfill(who.project).catch(() => {});
|
|
@@ -86,8 +77,7 @@ export function createMemoryHub(ctx, team, { store = createMemoryStore(team.root
|
|
|
86
77
|
const { results } = await hub.search(who, { query, k: 8 });
|
|
87
78
|
const block = store.format(results, MEMORY_LIMITS.injectChars);
|
|
88
79
|
recall.set(agent.id, { query, block, ids: results.map(r => r.memory.id) });
|
|
89
|
-
|
|
90
|
-
if (local.length) await store.markUsed(local.map(r => r.memory));
|
|
80
|
+
await core.markUsed(results.map(r => r.memory)); // backend hits are not in the store
|
|
91
81
|
},
|
|
92
82
|
recallText(agent) {
|
|
93
83
|
const r = recall.get(agent.id);
|
package/memory-store.js
CHANGED
|
@@ -7,6 +7,7 @@ import { appendFile, mkdir, readFile, rename, stat, unlink, writeFile } from 'no
|
|
|
7
7
|
import { join } from 'node:path';
|
|
8
8
|
import { cosine } from './memory-embed.js';
|
|
9
9
|
import { acquireLock } from './theme.js';
|
|
10
|
+
import { redactSecrets } from './memory-redact.js';
|
|
10
11
|
/** Several DSH processes (Desktop, `dsh web`, profiles) share the files: writes and compaction take a file lock. */
|
|
11
12
|
async function withFileLock(path, fn) {
|
|
12
13
|
let handle = null;
|
|
@@ -217,3 +218,53 @@ export function createMemoryStore(root, { now = Date.now, limits = MEMORY_LIMITS
|
|
|
217
218
|
for (const name of ['write', 'search', 'markUsed', 'unembedded', 'setVector', 'prune', 'remove']) store[name] = (...args) => serial(() => raw[name](...args));
|
|
218
219
|
return store;
|
|
219
220
|
}
|
|
221
|
+
|
|
222
|
+
// Shared write/search pipeline of the team memory hub (capability_memory_*) and @aiwuk/memory (memory_*): secret
|
|
223
|
+
// redaction, embedding, scope mapping (including the team `role` scope), write plus shared-backend sync and use
|
|
224
|
+
// counting live here once. Tool contracts, result shapes and recall policy stay with the two callers; the storage
|
|
225
|
+
// format is untouched, so existing memory files stay readable.
|
|
226
|
+
export const MEMORY_SCOPES = Object.freeze(['project', 'role', 'session', 'global']);
|
|
227
|
+
/** Store scope of a write. `role` is the team scope: visible to the same role of the same project. */
|
|
228
|
+
export function storeScope(scope, { project, session, role }) {
|
|
229
|
+
if (scope === 'global') return { kind: 'global' };
|
|
230
|
+
if (scope === 'project') return { kind: 'project', project };
|
|
231
|
+
if (scope === 'session') return { kind: 'session', project, session };
|
|
232
|
+
if (scope === 'role') return { kind: 'role', project, role };
|
|
233
|
+
throw new Error(`Unknown memory scope: ${scope}`);
|
|
234
|
+
}
|
|
235
|
+
export function createMemoryCore({ store, embedder, backend, secrets = () => [] }) {
|
|
236
|
+
/** Redact known credential values and token shapes before text is stored, embedded, synced or recalled. */
|
|
237
|
+
const clean = async text => redactSecrets(String(text ?? ''), await Promise.resolve(secrets()).catch(() => []));
|
|
238
|
+
/** One embedding as { vector, model }, or null when the embedder is unavailable or fails. */
|
|
239
|
+
async function embed(text, purpose) {
|
|
240
|
+
try { const out = await embedder.embed([text], purpose); return out ? { vector: out.vectors[0], model: out.model } : null; } catch { return null; }
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
clean,
|
|
244
|
+
embed,
|
|
245
|
+
/**
|
|
246
|
+
* Save one memory; `text` is redacted here, `key`/`tags` are prepared by the caller. Session scope never syncs.
|
|
247
|
+
* `sync` enables shared-backend sync, `vectorize` enables embedding. A sync failure is reported as `syncError`.
|
|
248
|
+
* Returns the store result plus { embedded, sync: 'local-only' | 'queued' | 'disabled' }.
|
|
249
|
+
*/
|
|
250
|
+
async write(who, { kind, text, scope, key = null, tags = [], source, project = who.project, vectorize = true, sync = true }) {
|
|
251
|
+
const body = await clean(text);
|
|
252
|
+
const embedded = vectorize ? await embed(body, 'passage') : null;
|
|
253
|
+
const saved = await store.write({ scope: storeScope(scope, { project, session: who.session, role: who.role }), kind, text: body, tags, key,
|
|
254
|
+
vector: embedded?.vector ?? null, model: embedded?.model ?? null, ...(source ? { source } : {}) });
|
|
255
|
+
const result = { ...saved, embedded: !!embedded, sync: scope === 'session' ? 'local-only' : 'disabled' };
|
|
256
|
+
if (scope !== 'session' && sync) {
|
|
257
|
+
try {
|
|
258
|
+
if (saved.superseded?.length) await backend.revoke?.(saved.superseded);
|
|
259
|
+
if (!saved.merged || saved.superseded?.length) result.sync = (await backend.enqueue(saved.memory, who)) ? 'queued' : 'disabled';
|
|
260
|
+
} catch (error) { result.syncError = error?.message ?? String(error); }
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
},
|
|
264
|
+
/** Count a recall hit on the local memories among `memories`; shared-backend rows are not in the store. */
|
|
265
|
+
async markUsed(memories) {
|
|
266
|
+
const local = memories.filter(m => MEMORY_SCOPES.includes(m?.scope?.kind));
|
|
267
|
+
if (local.length) await store.markUsed(local);
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -14,7 +14,7 @@ const definition = (key, init, apply) => ({ key, stateVersion: 1,
|
|
|
14
14
|
if (!Number.isSafeInteger(value.inherited) || value.inherited < 0 || !Number.isSafeInteger(value.cursor) || value.cursor < -1 || !value.view || value.view.schemaVersion !== 1) throw new Error('Invalid observability checkpoint');
|
|
15
15
|
if (key === 'capabilityTeamActivity' && (!Array.isArray(value.pending) || value.pending.length > 64 || !Array.isArray(value.view.recent) || value.view.recent.length > 8)) throw new Error('Invalid activity checkpoint');
|
|
16
16
|
if (key === 'capabilityTeamArtifacts' && (!Array.isArray(value.view.items) || value.view.items.length > 20 || !Array.isArray(value.view.changes) || value.view.changes.length > 10)) throw new Error('Invalid artifacts checkpoint');
|
|
17
|
-
if (key === 'capabilityTeamReport' && (!Array.isArray(value.pending) || value.pending.length > 64 || !value.view.usage || !value.view.
|
|
17
|
+
if (key === 'capabilityTeamReport' && (!Array.isArray(value.pending) || value.pending.length > 64 || !value.view.usage || !value.view.counters)) throw new Error('Invalid report checkpoint');
|
|
18
18
|
if (Buffer.byteLength(JSON.stringify(value.view)) > 32768) throw new Error('Oversized observability checkpoint');
|
|
19
19
|
return value;
|
|
20
20
|
} }, init, apply, wire: { viewSchema: schema, view: s => s.view } });
|
|
@@ -69,64 +69,39 @@ export const artifactsDefinition = definition('capabilityTeamArtifacts', (h, i)
|
|
|
69
69
|
return { ...s, cursor: e.seq, view: { schemaVersion: 1, items, changes, truncated } };
|
|
70
70
|
});
|
|
71
71
|
const zeroUsage = () => ({ uncachedInputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, outputTokens: 0 });
|
|
72
|
-
|
|
73
|
-
for (const part of stream ?? []) {
|
|
74
|
-
if (part.type === 'chunk') {
|
|
75
|
-
const c = part.chunk;
|
|
76
|
-
if ((['text-delta', 'reasoning-delta'].includes(c?.type) && c.text !== '') || (c?.type === 'tool-call-delta' && (c.argumentsDelta !== '' || c.name !== undefined))) return safeTime(part.time);
|
|
77
|
-
} else {
|
|
78
|
-
if (part.type === 'tool-call-chunks' && part.name !== undefined) return safeTime(part.time0);
|
|
79
|
-
const fragments = part.type === 'tool-call-chunks' ? part.args : part.texts;
|
|
80
|
-
let time = part.time0;
|
|
81
|
-
for (let i = 0; i < (fragments?.length ?? 0); i++) { if (i) time += part.dt?.[i - 1] ?? 0; if (fragments[i] !== '') return safeTime(time); }
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
export const reportDefinition = definition('capabilityTeamReport', (h, i) => ({ ...base(h, i), ownUsage: initOwnUsage(i), openStep: null, pending: [], lastTurn: null, view: { schemaVersion: 1, scope: 'own-session-events', firstTurnAt: null, lastTurnEndAt: null, openTurnAt: null, completedTurnMs: 0, turns: 0, steps: 0, stepsStarted: 0, usage: zeroUsage(), usageCoverage: { reportedSamples: 0, attemptsWithoutUsage: 0, coverage: 'unknown' }, timing: { llmMs: 0, toolMs: 0, ttftMs: 0, ttftSteps: 0, decodeMs: 0, decodeTokens: 0 }, counters: { hostRetriesStarted: 0, failedTurns: 0, sendMessageAccepted: 0, boardMutations: h?.origin === 'subagent' ? null : 0 }, truncated: false } }), (s, e) => {
|
|
72
|
+
export const reportDefinition = definition('capabilityTeamReport', (h, i) => ({ ...base(h, i), ownUsage: initOwnUsage(i), pending: [], lastTurn: null, view: { schemaVersion: 1, scope: 'own-session-events', firstTurnAt: null, lastTurnEndAt: null, openTurnAt: null, completedTurnMs: 0, turns: 0, steps: 0, stepsStarted: 0, usage: zeroUsage(), usageCoverage: { reportedSamples: 0, attemptsWithoutUsage: 0, coverage: 'unknown' }, counters: { hostRetriesStarted: 0, failedTurns: 0, sendMessageAccepted: 0, boardMutations: h?.origin === 'subagent' ? null : 0 }, truncated: false } }), (s, e) => {
|
|
87
73
|
if (ignored(s, e)) return s;
|
|
88
74
|
const relevant = ['request/header', 'turn/start', 'turn/end', 'step/start', 'step/end', 'assistant/attempt', 'assistant/message', 'llm/retry-started', 'tool/call', 'tool/result', 'team/task'];
|
|
89
75
|
if (!relevant.includes(e.type)) return s;
|
|
90
|
-
const d = e.data ?? {}, v = { ...s.view, usage: { ...s.view.usage },
|
|
76
|
+
const d = e.data ?? {}, v = { ...s.view, usage: { ...s.view.usage }, counters: { ...s.view.counters } };
|
|
91
77
|
const next = { ...s, cursor: e.seq, view: v }; const time = safeTime(e.time);
|
|
92
78
|
const add = (obj, key, value) => { if (typeof value === 'number' && value > Number.MAX_SAFE_INTEGER) v.truncated = true; const sum = obj[key] + n(value); if (!Number.isFinite(sum) || sum > Number.MAX_SAFE_INTEGER) v.truncated = true; obj[key] = n(sum); };
|
|
93
79
|
if (e.type === 'turn/start') { v.firstTurnAt ??= time; v.openTurnAt = time; }
|
|
94
80
|
if (e.type === 'turn/end') {
|
|
95
81
|
if (v.openTurnAt !== null && time !== null) add(v, 'completedTurnMs', Math.max(0, time - v.openTurnAt));
|
|
96
|
-
v.openTurnAt = null; v.lastTurnEndAt = time; next.pending = [];
|
|
82
|
+
v.openTurnAt = null; v.lastTurnEndAt = time; next.pending = [];
|
|
97
83
|
if (d.reason?.kind === 'error') add(v.counters, 'failedTurns', 1);
|
|
98
84
|
}
|
|
99
|
-
if (e.type === 'step/start') { add(v, 'stepsStarted', 1);
|
|
100
|
-
if (e.type === 'step/end') { add(v, 'steps', 1); if (s.lastTurn !== d.turn) add(v, 'turns', 1); next.lastTurn = d.turn;
|
|
85
|
+
if (e.type === 'step/start') { add(v, 'stepsStarted', 1); }
|
|
86
|
+
if (e.type === 'step/end') { add(v, 'steps', 1); if (s.lastTurn !== d.turn) add(v, 'turns', 1); next.lastTurn = d.turn; }
|
|
101
87
|
if (e.type === 'llm/retry-started') { add(v.counters, 'hostRetriesStarted', 1); }
|
|
102
88
|
if (e.type === 'tool/call' && safeId(d.callId)) {
|
|
103
|
-
next.pending = [...s.pending, { id: d.callId,
|
|
89
|
+
next.pending = [...s.pending, { id: d.callId, send: d.name === 'send_message' }].slice(-64);
|
|
104
90
|
if (s.pending.length >= 64) v.truncated = true;
|
|
105
91
|
}
|
|
106
92
|
if (e.type === 'tool/result') {
|
|
107
93
|
const id = callId(e), p = s.pending.find(p => p.id === id);
|
|
108
|
-
if (p) { next.pending = s.pending.filter(p => p.id !== id); if (
|
|
94
|
+
if (p) { next.pending = s.pending.filter(p => p.id !== id); if (p.send && !d.message?.isError) add(v.counters, 'sendMessageAccepted', 1); }
|
|
109
95
|
}
|
|
110
96
|
if (e.type === 'team/task' && v.counters.boardMutations !== null) add(v.counters, 'boardMutations', 1);
|
|
111
|
-
if (['assistant/message', 'assistant/attempt'].includes(e.type)) {
|
|
112
|
-
const open = s.openStep;
|
|
113
|
-
if (open && open.turn === d.turn && open.step === d.step) {
|
|
114
|
-
const first = open.first ?? firstToken(d.stream);
|
|
115
|
-
next.openStep = { ...open, first };
|
|
116
|
-
if (e.type === 'assistant/message') {
|
|
117
|
-
if (time !== null && open.at !== null) add(v.timing, 'llmMs', time - open.at);
|
|
118
|
-
if (first !== null && open.at !== null) { add(v.timing, 'ttftMs', first - open.at); add(v.timing, 'ttftSteps', 1); if (d.usage?.outputTokens != null) { add(v.timing, 'decodeMs', time - first); add(v.timing, 'decodeTokens', d.usage.outputTokens); } }
|
|
119
|
-
next.openStep = null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
97
|
next.ownUsage = applyOwnUsage(s.ownUsage, e);
|
|
124
98
|
v.usage = next.ownUsage.totals;
|
|
125
99
|
v.truncated ||= next.ownUsage.truncated;
|
|
126
100
|
v.usageCoverage = { reportedSamples: next.ownUsage.reportedSamples, attemptsWithoutUsage: next.ownUsage.attemptsWithoutUsage, coverage: next.ownUsage.coverage };
|
|
127
101
|
return next;
|
|
128
102
|
});
|
|
129
|
-
|
|
103
|
+
// Own-session attempt-aware fold. v4 dropped the `timing` view: turn/step wall times come from DSH's native `sessionStats` projection.
|
|
104
|
+
reportDefinition.stateVersion = 4;
|
|
130
105
|
export const ownUsageDefinition = {
|
|
131
106
|
key: OWN_USAGE_KEY, stateVersion: 1, stateSchema: { parse(v) { schema.parse(v); if (!Number.isSafeInteger(v.inherited) || !v.totals || !Array.isArray(v.routes)) throw new Error('Invalid own usage checkpoint'); return v; } },
|
|
132
107
|
init: (_header, inherited) => { const s = initOwnUsage(inherited); return { ...s, view: ownUsageView(s) }; },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwuk/team",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.8",
|
|
4
4
|
"description": "AIWUK Team, a capability-routed multi-agent team preset for DeepSeek Harness (DSH) with per-role model routing, same-tier failover and conversational team configuration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "walke2019",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"repository": { "type": "git", "url": "git+https://github.com/walke2019/dsh-capability-team.git" },
|
|
9
9
|
"bugs": { "url": "https://github.com/walke2019/dsh-capability-team/issues" },
|
|
10
10
|
"keywords": ["aiwuk", "dsh", "dsh-plugin", "deepseek-harness", "cordis", "agent-team", "multi-agent", "model-routing", "failover", "ai-agent", "llm"],
|
|
11
|
-
"engines": { "node": ">=
|
|
11
|
+
"engines": { "node": ">=24.9", "dsh": ">=0.2.2-rc.1 <0.3.0" },
|
|
12
12
|
"peerDependencies": {
|
|
13
13
|
"@deepseek-ai/cordis": ">=4.0.1",
|
|
14
14
|
"@deepseek-ai/dsh-agent-preset": ">=0.2.2-rc.1",
|
|
@@ -36,7 +36,8 @@
|
|
|
36
36
|
"type": "module",
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build:client": "node shared-ui/build.mjs && node scripts/build-client.mjs",
|
|
39
|
-
"pretest": "node shared-ui/build.mjs && node scripts/build-client.mjs",
|
|
39
|
+
"pretest": "node shared-ui/build.mjs && node scripts/build-client.mjs && node packages/global-memory/scripts/build-shared.mjs && node packages/omni-search/scripts/build-shared.mjs && node packages/autocomplete/scripts/build-client.mjs",
|
|
40
|
+
"prepack": "node shared-ui/build.mjs && node scripts/build-client.mjs",
|
|
40
41
|
"test": "node --import ./tests/isolated-home.mjs --test tests/*.test.mjs && node --import ./tests/isolated-home.mjs --test tests/global-memory/*.test.mjs && node --import ./tests/isolated-home.mjs --test tests/omni-search/*.test.mjs && node --import ./tests/isolated-home.mjs --test tests/autocomplete/*.test.mjs",
|
|
41
42
|
"build:global-memory": "node packages/global-memory/scripts/build-shared.mjs",
|
|
42
43
|
"build:omni-search": "node packages/omni-search/scripts/build-shared.mjs",
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"brand.js",
|
|
57
58
|
"i18n.js",
|
|
58
59
|
"roles.js",
|
|
60
|
+
"member-models.js",
|
|
59
61
|
"lead-route.js",
|
|
60
62
|
"config.js",
|
|
61
63
|
"roster-defaults.js",
|
package/roles.js
CHANGED
|
@@ -26,7 +26,7 @@ export function apply(ctx, team, failover, observability, taskControl) {
|
|
|
26
26
|
installed.add(agent);
|
|
27
27
|
agent.ctx.systemPrompt.context({ name: 'capability-team:policy', order: 95, text: () => team.owns(agent) ? policy : '' });
|
|
28
28
|
};
|
|
29
|
-
ctx.on('agent/created', ({ agent }) => installPolicy(agent));
|
|
29
|
+
ctx.on('agent/created', async ({ agent }) => { installPolicy(agent); await preselect(ctx, team, failover, agent); });
|
|
30
30
|
for (const agent of ctx.agents?.list?.() ?? []) installPolicy(agent);
|
|
31
31
|
const longContext = createLongContext();
|
|
32
32
|
const hadOverride = new WeakSet();
|
|
@@ -58,12 +58,48 @@ export function apply(ctx, team, failover, observability, taskControl) {
|
|
|
58
58
|
observability.request(agent, { turn, step, role, routed, selected });
|
|
59
59
|
} catch { /* telemetry must never change model selection */ }
|
|
60
60
|
}
|
|
61
|
+
// DSH fixes provider/model before step preparation; a different route can only take effect from the next step.
|
|
62
|
+
if (agent.model?.select && !sameRoute(previous, selected)) {
|
|
63
|
+
agent.model.select({ provider: selected.provider, model: selected.model });
|
|
64
|
+
ctx.logger?.info?.(`capability-team: ${previous.provider}/${previous.model} -> ${selected.provider}/${selected.model} from the next step`);
|
|
65
|
+
return previous;
|
|
66
|
+
}
|
|
67
|
+
if (agent.model?.select) return (lead && leadRequest(previous, selected)) || keepEffort(previous, allocatedRequest(previous, selected));
|
|
61
68
|
return (lead && leadRequest(previous, selected)) || allocatedRequest(previous, selected);
|
|
62
69
|
});
|
|
63
70
|
ctx.on('agent/request-error', async ({ agent, failure, signal }, next) => {
|
|
64
71
|
const downstream = await next(); // DSH provider retry policy keeps priority.
|
|
65
72
|
if (team.owns(agent)) { try { observability?.failure(agent, failure); } catch { /* read-only telemetry */ } }
|
|
66
73
|
if (downstream?.kind === 'retry' || !team.owns(agent)) return downstream;
|
|
67
|
-
|
|
74
|
+
const before = failover.status(agent)?.active;
|
|
75
|
+
if (!(await failover.recover(agent, failure, signal))) return undefined;
|
|
76
|
+
const after = failover.status(agent)?.active;
|
|
77
|
+
// A retry reuses the step's fixed route, so a switch is recorded for the next step instead of retried here.
|
|
78
|
+
if (agent.model?.select && after && after !== before) {
|
|
79
|
+
const slash = after.indexOf('/');
|
|
80
|
+
agent.model.select({ provider: after.slice(0, slash), model: after.slice(slash + 1) });
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
return { kind: 'retry' };
|
|
68
84
|
});
|
|
69
85
|
}
|
|
86
|
+
const sameRoute = (a, b) => a?.provider === b?.provider && a?.model === b?.model;
|
|
87
|
+
/** Same route as the step's selection: its reasoning effort is part of the fixed selection and must be kept. */
|
|
88
|
+
function keepEffort(previous, request) {
|
|
89
|
+
return previous.reasoningEffort === undefined ? request : { ...request, reasoningEffort: previous.reasoningEffort };
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Select a teammate's role route when it is created, before its first step fixes a model. Without this the
|
|
93
|
+
* teammate starts on the inherited Lead model and DSH rejects the per-role route in agent/request.
|
|
94
|
+
*/
|
|
95
|
+
export async function preselect(ctx, team, failover, agent) {
|
|
96
|
+
try {
|
|
97
|
+
if (!agent?.model?.select || !team.owns(agent) || ctx.agentTeams?.tryMembership?.(agent)?.role !== 'teammate') return;
|
|
98
|
+
const role = await team.resolve(agent);
|
|
99
|
+
if (role.unassigned || !role.provider) return; // agent/request fails closed with guidance
|
|
100
|
+
const route = failover.route(agent, 0, 0, role);
|
|
101
|
+
if (!sameRoute(agent.model.current?.(), route)) agent.model.select({ provider: route.provider, model: route.model });
|
|
102
|
+
} catch (error) {
|
|
103
|
+
ctx.logger?.warn?.(`capability-team: route preselection failed: ${String(error?.message ?? error)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/teammate-context.js
CHANGED
|
@@ -55,7 +55,7 @@ export function scopeTools(ctx) {
|
|
|
55
55
|
},
|
|
56
56
|
});
|
|
57
57
|
const scoped = new Proxy(ctx, { get: (target, prop) => (prop === 'tools' ? tools : target[prop]) });
|
|
58
|
-
ctx.on('agent/created', ({ agent }) => install(agent));
|
|
58
|
+
ctx.on('agent/created', ({ agent }) => { install(agent); });
|
|
59
59
|
ctx.on('agent/pre-step', (payload, next) => { install(payload?.agent); return next(); });
|
|
60
60
|
ctx.on('agent/disposed', ({ agent }) => uninstall(agent));
|
|
61
61
|
ctx.on('agent-preset/selected', sessionId => {
|
package/theme-plugin.js
CHANGED
|
@@ -157,7 +157,7 @@ export function apply(ctx, team, failover, workflow, planGate, taskControl) {
|
|
|
157
157
|
installed.set(agent, () => { removeReader(); removeWriter(); removeContext(); removeBudgetContext(); });
|
|
158
158
|
};
|
|
159
159
|
for (const agent of ctx.agents.list()) void install(agent);
|
|
160
|
-
ctx.on('agent/created', ({ agent }) => install(agent));
|
|
160
|
+
ctx.on('agent/created', async ({ agent }) => { await install(agent); });
|
|
161
161
|
ctx.on('agent/disposed', ({ agent }) => { installed.get(agent)?.(); installed.delete(agent); });
|
|
162
162
|
// A blank session reused for another preset is recomposed without agent/created or agent/disposed.
|
|
163
163
|
ctx.on('agent-preset/selected', sessionId => {
|
package/usage.js
CHANGED
|
@@ -5,6 +5,10 @@ import { m as modelText } from './i18n.js';
|
|
|
5
5
|
// each Assistant settlement contributes its last usage sample; a failed assistant/attempt
|
|
6
6
|
// and llm/retry-started close the slot. Same-slot message corrections replace it. Samples are attributed to the
|
|
7
7
|
// route of the latest request header, which is the route the attempt was sent to.
|
|
8
|
+
// Why this fold stays instead of DSH's native `tokenUsage` projection: the native one has no route dimension (no
|
|
9
|
+
// per-model rows, no lastRoute) and its init ignores the fork-inherited prefix, so a forked member would be charged
|
|
10
|
+
// for its parent's tokens and role budgets (maxTokens) would trip early. Wall times (turns, steps, llm/tool/ttft/decode)
|
|
11
|
+
// are not folded here; they come from the native `sessionStats` projection.
|
|
8
12
|
const BUCKETS = ['uncachedInputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'];
|
|
9
13
|
export const zeroUsage = () => ({ uncachedInputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 });
|
|
10
14
|
const numeric = value => Number.isFinite(value) && value >= 0 ? Math.min(value, Number.MAX_SAFE_INTEGER) : 0;
|