@wowyuarm/dsh-agent-team 0.1.9 → 0.1.11
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 +17 -11
- package/README.zh.md +17 -11
- package/package.json +61 -47
- package/packages/agent-team/README.md +6 -4
- package/packages/agent-team/README.zh.md +6 -3
- package/packages/agent-team/lib/context-management.js +48 -55
- package/packages/agent-team/lib/context-projection.js +22 -8
- package/packages/agent-team/lib/context-source.js +163 -23
- package/packages/agent-team/lib/index.js +349 -176
- package/packages/agent-team/lib/invariant.js +38 -4
- package/packages/agent-team/lib/ledger.js +658 -294
- package/packages/agent-team/lib/member-context.js +6 -2
- package/packages/agent-team/lib/member-time-context.js +141 -0
- package/packages/agent-team/lib/pressure-policy.js +34 -11
- package/packages/agent-team/lib/progress-nudge.js +12 -4
- package/packages/agent-team/lib/session-event-cursor.js +116 -0
- package/packages/agent-team/lib/session-remediation.js +493 -0
- package/packages/agent-team/lib/spec.js +49 -36
- package/packages/agent-team/lib/stored-session-reader.js +138 -0
- package/packages/agent-team/lib/time-format.js +56 -0
- package/packages/agent-team/lib/typert.host.js +163 -63
- package/packages/agent-team/lib/typert.remote-client.d.ts.map +1 -1
- package/packages/agent-team/lib/typert.remote-client.js +105 -32
- package/packages/agent-team/lib/types/context-management.d.ts +13 -1
- package/packages/agent-team/lib/types/context-management.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-projection.d.ts +8 -0
- package/packages/agent-team/lib/types/context-projection.d.ts.map +1 -1
- package/packages/agent-team/lib/types/context-source.d.ts +70 -36
- package/packages/agent-team/lib/types/context-source.d.ts.map +1 -1
- package/packages/agent-team/lib/types/index.d.ts +110 -9
- package/packages/agent-team/lib/types/index.d.ts.map +1 -1
- package/packages/agent-team/lib/types/invariant.d.ts.map +1 -1
- package/packages/agent-team/lib/types/ledger.d.ts +205 -7
- package/packages/agent-team/lib/types/ledger.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-context.d.ts.map +1 -1
- package/packages/agent-team/lib/types/member-time-context.d.ts +83 -0
- package/packages/agent-team/lib/types/member-time-context.d.ts.map +1 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts +18 -0
- package/packages/agent-team/lib/types/pressure-policy.d.ts.map +1 -1
- package/packages/agent-team/lib/types/progress-nudge.d.ts.map +1 -1
- package/packages/agent-team/lib/types/session-event-cursor.d.ts +119 -0
- package/packages/agent-team/lib/types/session-event-cursor.d.ts.map +1 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts +172 -0
- package/packages/agent-team/lib/types/session-remediation.d.ts.map +1 -0
- package/packages/agent-team/lib/types/spec.d.ts.map +1 -1
- package/packages/agent-team/lib/types/stored-session-reader.d.ts +92 -0
- package/packages/agent-team/lib/types/stored-session-reader.d.ts.map +1 -0
- package/packages/agent-team/lib/types/time-format.d.ts +28 -0
- package/packages/agent-team/lib/types/time-format.d.ts.map +1 -0
- package/packages/agent-team/lib/types/types/entities.d.ts +36 -2
- package/packages/agent-team/lib/types/types/entities.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/operations.d.ts +45 -19
- package/packages/agent-team/lib/types/types/operations.d.ts.map +1 -1
- package/packages/agent-team/lib/types/types/requests-results.d.ts +50 -3
- package/packages/agent-team/lib/types/types/requests-results.d.ts.map +1 -1
- package/packages/agent-team/preset/team-member/agent.cordis.yml +21 -4
- package/packages/client-agent-team/README.md +1 -1
- package/packages/client-agent-team/README.zh.md +1 -1
- package/packages/client-agent-team/lib/client.js +1121 -402
- package/packages/client-agent-team/lib/client.js.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamAgentsPanel.js +24 -4
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelPage.js +42 -22
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamChannelsPanel.js +13 -24
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts +11 -5
- package/packages/client-agent-team/lib/types/client/TeamComposer.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamComposer.js +15 -9
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamConversation.js +8 -1
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts +21 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/TeamInboxPage.js +71 -0
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMemberEditor.js +8 -24
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts +9 -2
- package/packages/client-agent-team/lib/types/client/TeamMessage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamMessage.js +13 -6
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts +8 -0
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamPresenceDot.js +21 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamThreadPage.js +106 -32
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/TeamWorkspaceBrowser.js +50 -7
- package/packages/client-agent-team/lib/types/client/index.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/index.js +25 -9
- package/packages/client-agent-team/lib/types/client/locales.d.ts +18 -0
- package/packages/client-agent-team/lib/types/client/locales.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/locales.js +18 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts +10 -0
- package/packages/client-agent-team/lib/types/client/navigation.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/navigation.js +18 -7
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts +41 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/scope-coverage.js +64 -0
- package/packages/client-agent-team/lib/types/client/slots.d.ts +8 -2
- package/packages/client-agent-team/lib/types/client/slots.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts +26 -1
- package/packages/client-agent-team/lib/types/client/team-changes.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-changes.js +66 -15
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts +20 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.d.ts.map +1 -0
- package/packages/client-agent-team/lib/types/client/team-dialog-save.js +36 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts +9 -0
- package/packages/client-agent-team/lib/types/client/team-formatters.d.ts.map +1 -1
- package/packages/client-agent-team/lib/types/client/team-formatters.js +34 -6
- package/packages/tool-agent-team/README.md +8 -6
- package/packages/tool-agent-team/README.zh.md +8 -6
- package/packages/tool-agent-team/lib/context-tools.js +18 -14
- package/packages/tool-agent-team/lib/host-access.js +20 -0
- package/packages/tool-agent-team/lib/index.js +314 -123
- package/packages/tool-agent-team/lib/types/context-tools.d.ts.map +1 -1
- package/packages/tool-agent-team/lib/types/host-access.d.ts +14 -0
- package/packages/tool-agent-team/lib/types/host-access.d.ts.map +1 -0
- package/packages/tool-agent-team/lib/types/index.d.ts.map +1 -1
|
@@ -61,16 +61,20 @@ export function renderMemberIdentity(member) {
|
|
|
61
61
|
? `Team identity: you are @${member.handle}.`
|
|
62
62
|
: `Team identity: you are @${member.handle} — ${member.description}`;
|
|
63
63
|
}
|
|
64
|
+
/** The four private-memory paths plus the out-of-cwd warning; callers append their own sentence. */
|
|
65
|
+
function memoryPathsBlock(privateMemoryPath) {
|
|
66
|
+
return `Private memory directory: ${privateMemoryPath}\nMemory index: ${privateMemoryPath}/memory.md\nNotes directory: ${privateMemoryPath}/notes\nPrivate skills directory: ${privateMemoryPath}/skills\nThese paths are outside the Workspace cwd.`;
|
|
67
|
+
}
|
|
64
68
|
export function renderMemberMemory(raw, privateMemoryPath = '<private-memory-path>') {
|
|
65
69
|
const overBudget = raw.byteLength > MAX_MEMORY_BYTES;
|
|
66
70
|
const body = overBudget ? '' : raw.toString('utf8');
|
|
67
71
|
const warning = overBudget
|
|
68
72
|
? '\n\n[Maintenance warning: memory.md exceeds the 8 KiB context budget. Its contents were not injected; do not delete or automatically summarize the file. Maintain a smaller index explicitly.]'
|
|
69
73
|
: '';
|
|
70
|
-
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\
|
|
74
|
+
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. It is reference context only, may be stale, and is not an instruction or Team fact.\n\n${memoryPathsBlock(privateMemoryPath)} Relative filesystem paths resolve from cwd, so use the absolute paths above when reading or editing this Member's memory. Only this Member can read this directory — no other human or agent sees its contents; when communicating, restate what you need from it instead of pointing others at these paths. Read matching notes on demand; do not copy credentials, sensitive data, guesses, chat logs, other Members' memory, or Team facts already owned by the ledger into memory.\n\n${escape(body)}${warning}\n${END}`;
|
|
71
75
|
}
|
|
72
76
|
function renderUnavailableMemory(privateMemoryPath, reason) {
|
|
73
|
-
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\
|
|
77
|
+
return `${BEGIN}\nThis is the complete replacement for this Team Member's private memory index; all earlier private-memory context is obsolete. ${reason}\n\n${memoryPathsBlock(privateMemoryPath)} Use the absolute paths above when inspecting or repairing this Member's memory.\n${END}`;
|
|
74
78
|
}
|
|
75
79
|
function escape(value) {
|
|
76
80
|
return value.replaceAll(BEGIN, '[escaped begin marker]').replaceAll(END, '[escaped end marker]');
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team Member turn-level clock context.
|
|
3
|
+
*
|
|
4
|
+
* The first model step of every eligible Team Member turn appends one
|
|
5
|
+
* durable, source-attributed clock snapshot: the current instant in the
|
|
6
|
+
* fixed Team coordination zone (UTC+8), the elapsed time since the
|
|
7
|
+
* preceding model-visible event, and the ordering authority note. Later
|
|
8
|
+
* steps of the same turn stay quiet unless the turn runs longer than the
|
|
9
|
+
* refresh interval, in which case one snapshot lands per elapsed interval —
|
|
10
|
+
* a tool-dense turn of quick steps produces exactly one line, while a turn
|
|
11
|
+
* that outlives the interval still shows its real span. The snapshot is an
|
|
12
|
+
* observation, never ledger authority: sequence and revision, not
|
|
13
|
+
* wall-clock time, order Team facts.
|
|
14
|
+
*
|
|
15
|
+
* This plugin deliberately does not mount the shipped
|
|
16
|
+
* `@deepseek-ai/dsh-time-context`: its browser-zone policy asks the model to
|
|
17
|
+
* confirm dates with the user whenever a request carries no unique browser
|
|
18
|
+
* zone, which is the normal case for background Member wakes (Inbox, DM,
|
|
19
|
+
* recovery, continuation). The Team coordination zone is fixed instead. If
|
|
20
|
+
* the harness grows a public non-browser/canonical-zone policy, retire this
|
|
21
|
+
* row in favor of configuring that plugin.
|
|
22
|
+
*
|
|
23
|
+
* State is folded from the Member Session's own events — the same
|
|
24
|
+
* manual-fold pattern the Host's context projection uses — so restart,
|
|
25
|
+
* request reconstruction, and compaction all derive identical baselines
|
|
26
|
+
* without a second durable store.
|
|
27
|
+
* @module @wowyuarm/dsh-agent-team/member-time-context
|
|
28
|
+
*/
|
|
29
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
30
|
+
import { formatTeamDuration, formatTeamTimestamp } from "./time-format.js";
|
|
31
|
+
import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
|
|
32
|
+
export const name = 'wowyuarm-agent-team-member-time-context';
|
|
33
|
+
/** Default minimum spacing between two snapshots within one turn, in ms. */
|
|
34
|
+
export const CLOCK_REFRESH_INTERVAL_MS = 1_800_000;
|
|
35
|
+
function emptyBaseline() {
|
|
36
|
+
return { lastMessageTime: null, lastInjectionTime: null, lastTurnInjectionTime: null, openTurn: -1 };
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fold one session event into the clock baseline. Uninterested events return
|
|
40
|
+
* the same state reference.
|
|
41
|
+
* @internal exported for tests.
|
|
42
|
+
*/
|
|
43
|
+
export function applyClockEvent(state, event) {
|
|
44
|
+
switch (event.type) {
|
|
45
|
+
case 'turn/start':
|
|
46
|
+
return event.data.turn === state.openTurn ? state
|
|
47
|
+
: { ...state, lastTurnInjectionTime: null, openTurn: event.data.turn };
|
|
48
|
+
case 'turn/end':
|
|
49
|
+
return state.openTurn === -1 ? state : { ...state, lastTurnInjectionTime: null, openTurn: -1 };
|
|
50
|
+
case 'user/message': {
|
|
51
|
+
const source = event.data.source;
|
|
52
|
+
const injected = source.kind === 'plugin' && source.plugin === name;
|
|
53
|
+
const withMessage = state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time };
|
|
54
|
+
if (!injected)
|
|
55
|
+
return withMessage;
|
|
56
|
+
return { ...withMessage, lastInjectionTime: event.time, lastTurnInjectionTime: event.time };
|
|
57
|
+
}
|
|
58
|
+
case 'assistant/message':
|
|
59
|
+
case 'tool/result':
|
|
60
|
+
return state.lastMessageTime === event.time ? state : { ...state, lastMessageTime: event.time };
|
|
61
|
+
default:
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Fold a whole event log into the clock baseline. @internal exported for tests. */
|
|
66
|
+
export function foldClockBaseline(events) {
|
|
67
|
+
let state = emptyBaseline();
|
|
68
|
+
for (const event of events)
|
|
69
|
+
state = applyClockEvent(state, event);
|
|
70
|
+
return state;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Whether this step should append a clock snapshot: the first step of a turn
|
|
74
|
+
* always does (every wake starts with a fresh instant), and a later step
|
|
75
|
+
* does only when the turn has run longer than the refresh interval since
|
|
76
|
+
* the last landed snapshot. Skipped steps produce nothing and never
|
|
77
|
+
* backfill — their span folds into the next snapshot's elapsed.
|
|
78
|
+
* @internal exported for tests.
|
|
79
|
+
*/
|
|
80
|
+
export function shouldSampleClock(step, now, baseline, refreshIntervalMs) {
|
|
81
|
+
if (step === 1)
|
|
82
|
+
return true;
|
|
83
|
+
return baseline.lastTurnInjectionTime === null || now - baseline.lastTurnInjectionTime >= refreshIntervalMs;
|
|
84
|
+
}
|
|
85
|
+
/** Render one durable clock snapshot text. @internal exported for tests. */
|
|
86
|
+
export function renderClockSnapshot(input) {
|
|
87
|
+
// A wall-clock rollback clamps elapsed to 0s without rewriting history.
|
|
88
|
+
const elapsed = input.previous === undefined ? 'unavailable' : formatTeamDuration(input.now - input.previous);
|
|
89
|
+
const baseline = input.step === 1 ? 'model-visible event' : 'step context';
|
|
90
|
+
return `Team clock sampled while preparing turn ${input.turn}, step ${input.step}: ${formatTeamTimestamp(new Date(input.now).toISOString())}\n`
|
|
91
|
+
+ `Elapsed since the preceding ${baseline}: ${elapsed}.\n`
|
|
92
|
+
+ 'Team collaboration timestamps use UTC+8. Sequence and revision, not wall-clock time, determine ordering and concurrency.';
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The clock baseline folded incrementally over one Session's own events. A
|
|
96
|
+
* rollover starts a fresh Session log, and the cache below is keyed by Session
|
|
97
|
+
* id, so the fold never guesses elapsed across generations: a missing prior
|
|
98
|
+
* event renders `unavailable`, not a fabricated baseline.
|
|
99
|
+
*/
|
|
100
|
+
const CLOCK_FOLD = {
|
|
101
|
+
start: emptyBaseline(),
|
|
102
|
+
step: (state, event) => applyClockEvent(state, event),
|
|
103
|
+
};
|
|
104
|
+
export function apply(ctx, config = {}) {
|
|
105
|
+
const refreshIntervalMs = config.refreshIntervalMs ?? CLOCK_REFRESH_INTERVAL_MS;
|
|
106
|
+
// Per-step fold state, reused for this plugin lifecycle: one entry per
|
|
107
|
+
// Member, replaced when that Member's Session changes, so a rollover neither
|
|
108
|
+
// resumes across generations nor retains every generation it leaves behind.
|
|
109
|
+
const cursors = new Map();
|
|
110
|
+
ctx.on('agent/pre-step', async ({ agent, turn, step, signal }, next) => {
|
|
111
|
+
const decision = await next();
|
|
112
|
+
if (decision.kind === 'reject' || signal.aborted)
|
|
113
|
+
return decision;
|
|
114
|
+
// The Host service is resolved at step time, never through plugin inject
|
|
115
|
+
// (same reason as member-context: the row mounts while the Host itself
|
|
116
|
+
// is still restoring Members).
|
|
117
|
+
const host = ctx.get('agentTeam');
|
|
118
|
+
if (host === undefined)
|
|
119
|
+
return decision;
|
|
120
|
+
const member = host.memberForAgent(agent);
|
|
121
|
+
if (member === undefined)
|
|
122
|
+
return decision;
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const owned = advanceOwnedSessionEventCursor(cursors.get(member.memberId), agent.session.id, CLOCK_FOLD, agent.session.ownEvents(), agent.session.inheritedEventCount);
|
|
125
|
+
cursors.set(member.memberId, owned);
|
|
126
|
+
const baseline = owned.cursor.value;
|
|
127
|
+
// Turn-first-step always samples; later steps sample only at the refresh
|
|
128
|
+
// interval, so a quick tool-dense turn stays at one line.
|
|
129
|
+
if (!shouldSampleClock(step, now, baseline, refreshIntervalMs))
|
|
130
|
+
return decision;
|
|
131
|
+
const previous = step === 1
|
|
132
|
+
? baseline.lastMessageTime ?? undefined
|
|
133
|
+
: baseline.lastTurnInjectionTime ?? undefined;
|
|
134
|
+
const text = renderClockSnapshot({ now, turn, step, previous });
|
|
135
|
+
const message = createUserMessage({
|
|
136
|
+
content: [{ type: 'text', text }],
|
|
137
|
+
source: { kind: 'plugin', plugin: name, form: 'snapshot', sections: [{ name, text }] },
|
|
138
|
+
});
|
|
139
|
+
return { kind: 'enter', messages: [...decision.messages, message] };
|
|
140
|
+
}, { prepend: true });
|
|
141
|
+
}
|
|
@@ -17,8 +17,27 @@
|
|
|
17
17
|
import { CONTEXT_WINDOW_EXCEEDED_CODE } from '@deepseek-ai/dsh-llm';
|
|
18
18
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
19
19
|
import { CONTEXT_PRESSURE_NOTICE_SUMMARY } from "./context-management.js";
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
import { AGENT_TEAM_PLUGIN_ID } from "./context-source.js";
|
|
21
|
+
import { advanceOwnedSessionEventCursor } from "./session-event-cursor.js";
|
|
22
|
+
/**
|
|
23
|
+
* Whether this Session's own span already carries the pressure notice: either
|
|
24
|
+
* surfaced as a `user/message` or still queued in a durable
|
|
25
|
+
* `agent/inbox/spliced` insert. Expressed as a fold rather than a scan because
|
|
26
|
+
* the answer is monotone — once delivered it stays delivered for the
|
|
27
|
+
* generation — so the cursor only ever has to read events it has not seen.
|
|
28
|
+
*/
|
|
29
|
+
export const PRESSURE_NOTICE_FOLD = {
|
|
30
|
+
start: false,
|
|
31
|
+
step: (delivered, event) => {
|
|
32
|
+
if (delivered)
|
|
33
|
+
return true;
|
|
34
|
+
if (event.type === 'user/message' && isPressureNotice(event.data))
|
|
35
|
+
return true;
|
|
36
|
+
if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
|
|
37
|
+
return true;
|
|
38
|
+
return false;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
22
41
|
/** One pressure-notice text; concise, structured, and inside the reserve. */
|
|
23
42
|
export function contextPressureNoticeText(input) {
|
|
24
43
|
const claims = input.activeClaims.length === 0 ? 'none' : input.activeClaims.join(', ');
|
|
@@ -42,6 +61,14 @@ export class PressurePolicyCoordinator {
|
|
|
42
61
|
* Process-only by design: a restart re-earns one sequence per chain.
|
|
43
62
|
*/
|
|
44
63
|
overflowRetries = new Map();
|
|
64
|
+
/**
|
|
65
|
+
* Whether the one-shot notice was already delivered, folded incrementally per
|
|
66
|
+
* Member. The scan below is a monotone "has this ever happened" fold over the
|
|
67
|
+
* Session's own events, so a cursor can replace re-scanning the whole log on
|
|
68
|
+
* every step; identity guarding falls back to a cold fold when the Member's
|
|
69
|
+
* Session changed under the entry.
|
|
70
|
+
*/
|
|
71
|
+
noticeSeen = new Map();
|
|
45
72
|
disposed = false;
|
|
46
73
|
constructor(options) {
|
|
47
74
|
this.options = options;
|
|
@@ -56,14 +83,10 @@ export class PressurePolicyCoordinator {
|
|
|
56
83
|
* rollover starts a fresh Session whose own event span has no notice yet,
|
|
57
84
|
* which is exactly the documented re-arm.
|
|
58
85
|
*/
|
|
59
|
-
noticeDelivered(agent) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (event.type === 'agent/inbox/spliced' && event.data.inserted.some(isPressureNotice))
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
return false;
|
|
86
|
+
noticeDelivered(agent, memberId) {
|
|
87
|
+
const owned = advanceOwnedSessionEventCursor(this.noticeSeen.get(memberId), agent.session.id, PRESSURE_NOTICE_FOLD, agent.session.ownEvents(), agent.session.inheritedEventCount);
|
|
88
|
+
this.noticeSeen.set(memberId, owned);
|
|
89
|
+
return owned.cursor.value;
|
|
67
90
|
}
|
|
68
91
|
dispose() {
|
|
69
92
|
this.disposed = true;
|
|
@@ -99,7 +122,7 @@ export class PressurePolicyCoordinator {
|
|
|
99
122
|
const outcome = await this.enforceHardLimit(agent, member.memberId, member.sessionId, signal);
|
|
100
123
|
return outcome ? { kind: 'continue' } : { kind: 'reject' };
|
|
101
124
|
}
|
|
102
|
-
if (usageTokens >= handoffAt && !this.noticeDelivered(agent)) {
|
|
125
|
+
if (usageTokens >= handoffAt && !this.noticeDelivered(agent, member.memberId)) {
|
|
103
126
|
const notice = createUserMessage({
|
|
104
127
|
content: [{ type: 'text', text: contextPressureNoticeText({
|
|
105
128
|
usageTokens, handoffAt, hardLimit,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* @module @wowyuarm/dsh-agent-team/progress-nudge
|
|
23
23
|
*/
|
|
24
24
|
import { createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
25
|
-
|
|
25
|
+
import { AGENT_TEAM_PLUGIN_ID, isCheckpointContinuationMessage } from "./context-source.js";
|
|
26
26
|
/** Notice summary for every nudge this Coordinator injects. */
|
|
27
27
|
export const PROGRESS_NUDGE_NOTICE_SUMMARY = 'Progress visibility reminder';
|
|
28
28
|
export const PROGRESS_NUDGE_TOOL_CALLS_START = 20;
|
|
@@ -39,7 +39,7 @@ export function isPublicCommunicationOperationKind(kind) {
|
|
|
39
39
|
/** Whether one message is a nudge notice this module injected. */
|
|
40
40
|
export function isProgressNudgeNotice(message) {
|
|
41
41
|
const source = message.source;
|
|
42
|
-
return source.kind === 'plugin' && source.plugin ===
|
|
42
|
+
return source.kind === 'plugin' && source.plugin === AGENT_TEAM_PLUGIN_ID
|
|
43
43
|
&& source.form === 'notice' && source.summary === PROGRESS_NUDGE_NOTICE_SUMMARY;
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
@@ -218,7 +218,7 @@ export class ProgressNudgeCoordinator {
|
|
|
218
218
|
const claimTargets = claimDue ? freshClaimTargets : [];
|
|
219
219
|
const notice = createUserMessage({
|
|
220
220
|
content: [{ type: 'text', text: nudgeNoticeText(state.silentToolCalls, progressTargets, claimTargets) }],
|
|
221
|
-
source: { kind: 'plugin', plugin:
|
|
221
|
+
source: { kind: 'plugin', plugin: AGENT_TEAM_PLUGIN_ID, form: 'notice', summary: PROGRESS_NUDGE_NOTICE_SUMMARY },
|
|
222
222
|
});
|
|
223
223
|
const pending = {
|
|
224
224
|
messageId: notice.id,
|
|
@@ -268,7 +268,15 @@ export class ProgressNudgeCoordinator {
|
|
|
268
268
|
hasBlockingNotice(agent) {
|
|
269
269
|
for (const message of [...agent.inbox.nextStep, ...agent.inbox.nextTurn]) {
|
|
270
270
|
const source = message.source;
|
|
271
|
-
if (source.kind !== 'plugin' || source.plugin !==
|
|
271
|
+
if (source.kind !== 'plugin' || source.plugin !== AGENT_TEAM_PLUGIN_ID)
|
|
272
|
+
continue;
|
|
273
|
+
// A queued checkpoint continuation is a pending wake that outranks a
|
|
274
|
+
// nudge exactly as the notice families above do. It rides the snapshot
|
|
275
|
+
// form (it must carry its checkpoint ref in a section), so it cannot be
|
|
276
|
+
// recognized by the notice summary check below.
|
|
277
|
+
if (isCheckpointContinuationMessage(message))
|
|
278
|
+
return true;
|
|
279
|
+
if (source.form !== 'notice')
|
|
272
280
|
continue;
|
|
273
281
|
if (source.summary !== PROGRESS_NUDGE_NOTICE_SUMMARY)
|
|
274
282
|
return true;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Incremental Session-log fold with an identity guard.
|
|
3
|
+
*
|
|
4
|
+
* Several Host projections are pure left folds over one Member Session's own
|
|
5
|
+
* events: context intent, the clock baseline, and the one-shot pressure
|
|
6
|
+
* notice. Folding the whole log on every event makes each of them cost
|
|
7
|
+
* O(session length) per step, so a long Session gets steadily slower. This
|
|
8
|
+
* module keeps the fold value together with how far it has consumed the log,
|
|
9
|
+
* so a step folds only the tail that arrived since — the same idiom the
|
|
10
|
+
* Harness Session itself uses for its own derived folds.
|
|
11
|
+
*
|
|
12
|
+
* The module is deliberately Session-agnostic: it takes a plain event array
|
|
13
|
+
* plus the log offset that array starts at, so a caller (including a
|
|
14
|
+
* measurement harness) can drive it without constructing a Session.
|
|
15
|
+
*
|
|
16
|
+
* Correctness rests on the log being append-only, which it is: Session logs
|
|
17
|
+
* have no truncation API, `seq` is the log index, and `seq === log.length` is
|
|
18
|
+
* a documented contiguity contract. A Session adopts a different log only by
|
|
19
|
+
* being constructed from another seed, which replaces the whole slice rather
|
|
20
|
+
* than editing one position.
|
|
21
|
+
*
|
|
22
|
+
* The guard covers the observable ways that contract can break: a changed
|
|
23
|
+
* `logFrom`, a shorter log, a different event type at the anchor. Each
|
|
24
|
+
* re-folds from `logFrom`, so a fork, resume, or rollover degrades to the cost
|
|
25
|
+
* of a cold fold and never to a wrong picture.
|
|
26
|
+
*
|
|
27
|
+
* Named residual: the guard anchors on `(seq, type)`, so an event replaced in
|
|
28
|
+
* place by another of the SAME type at the SAME seq is not detected, and the
|
|
29
|
+
* cursor would keep its old value and fold only the tail. Reaching that state
|
|
30
|
+
* requires a caller to violate the append-only contract above — reading one
|
|
31
|
+
* Session's cursor against a log someone rewrote in place. A content-sensitive
|
|
32
|
+
* anchor would not close it either: only the anchor position is re-read, so a
|
|
33
|
+
* swap at any earlier folded position passes a boundary digest just as
|
|
34
|
+
* silently, while costing a payload digest on every advance. Closing it
|
|
35
|
+
* properly means fingerprinting every folded event, which is the full refold
|
|
36
|
+
* this module exists to avoid; if the contract ever changes, that is the fix.
|
|
37
|
+
* @module @wowyuarm/dsh-agent-team/session-event-cursor
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Open a cursor for a log slice that starts at `logFrom` (a Session's
|
|
41
|
+
* `inheritedEventCount` for its own events, or 0 for a full log). Nothing is
|
|
42
|
+
* consumed yet, so the first {@link advanceSessionEventCursor} folds the whole
|
|
43
|
+
* slice.
|
|
44
|
+
*/
|
|
45
|
+
export function initSessionEventCursor(fold, logFrom = 0) {
|
|
46
|
+
return { value: fold.start, foldedThrough: logFrom, logFrom, anchor: null };
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fold the log tail that arrived since the cursor stopped and return the
|
|
50
|
+
* advanced cursor; the same reference comes back when there is nothing new.
|
|
51
|
+
*
|
|
52
|
+
* `events` is the slice starting at `logFrom`, and `logEnd` is the EXCLUSIVE
|
|
53
|
+
* end offset of that slice in the owning log — the same coordinate space as
|
|
54
|
+
* `logFrom`, not `events.length`. An inherited Session's own events start at
|
|
55
|
+
* its `inheritedEventCount`, so for a 14-event log with 10 inherited events
|
|
56
|
+
* the slice is `events[0..4]` spanning `logFrom = 10` to `logEnd = 14`. This
|
|
57
|
+
* mirrors the Session's own `snapshotEvents(fromSeq, toSeqExclusive)`.
|
|
58
|
+
*
|
|
59
|
+
* Both offsets are passed explicitly rather than read from the cursor so the
|
|
60
|
+
* caller states the log it is folding, which is also what makes the fallback
|
|
61
|
+
* below decidable.
|
|
62
|
+
*/
|
|
63
|
+
export function advanceSessionEventCursor(cursor, events, logFrom, logEnd, fold) {
|
|
64
|
+
if (logEnd !== logFrom + events.length) {
|
|
65
|
+
// The slice and the log it claims to describe disagree, so the tail it is
|
|
66
|
+
// missing is indistinguishable from events that were never there. Refuse
|
|
67
|
+
// rather than fold a shape the caller did not state.
|
|
68
|
+
throw new RangeError(`session event cursor: ${events.length} events cannot span [${logFrom}, ${logEnd})`);
|
|
69
|
+
}
|
|
70
|
+
// Resume only from a position the log can still justify. Every other case
|
|
71
|
+
// re-folds from `logFrom`: one path handles truncation, a changed fork
|
|
72
|
+
// prefix, and a rebuilt log alike, so no caller has to classify them.
|
|
73
|
+
const resumable = cursor.logFrom === logFrom
|
|
74
|
+
&& cursor.foldedThrough >= logFrom
|
|
75
|
+
&& cursor.foldedThrough <= logEnd
|
|
76
|
+
&& (cursor.foldedThrough === logFrom || anchorMatches(cursor, events));
|
|
77
|
+
const from = resumable ? cursor.foldedThrough : logFrom;
|
|
78
|
+
if (from === logEnd)
|
|
79
|
+
return cursor;
|
|
80
|
+
let value = resumable ? cursor.value : fold.start;
|
|
81
|
+
for (let index = from; index < logEnd; index += 1)
|
|
82
|
+
value = fold.step(value, events[index - logFrom]);
|
|
83
|
+
return { value, foldedThrough: logEnd, logFrom, anchor: anchorOf(events[logEnd - 1 - logFrom]) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Whether the event the cursor stopped on still occupies that position with
|
|
87
|
+
* the type it had. The position is checked as well as the type because `seq`
|
|
88
|
+
* is a log index: an event that moved is a different event.
|
|
89
|
+
*/
|
|
90
|
+
function anchorMatches(cursor, events) {
|
|
91
|
+
const anchor = cursor.anchor;
|
|
92
|
+
if (anchor === null)
|
|
93
|
+
return false;
|
|
94
|
+
const seen = events[anchor.seq - cursor.logFrom];
|
|
95
|
+
return seen !== undefined && seen.seq === anchor.seq && seen.type === anchor.type;
|
|
96
|
+
}
|
|
97
|
+
/** The identity a cursor anchors on when it stops at the last consumed event. */
|
|
98
|
+
function anchorOf(event) {
|
|
99
|
+
if (event === undefined)
|
|
100
|
+
return null;
|
|
101
|
+
return { seq: event.seq, type: event.type };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Advance one owner's fold and answer with the entry to store back, so every
|
|
105
|
+
* caller keeps the same one-entry-per-owner invariant.
|
|
106
|
+
*
|
|
107
|
+
* `events` is that Session's whole own-events slice starting at `logFrom` (its
|
|
108
|
+
* `inheritedEventCount`), so the slice's end is the log's end. The cursor is
|
|
109
|
+
* resumed only while `owned` still belongs to `sessionId`; every other case
|
|
110
|
+
* starts cold at `logFrom`, which is the same degradation the cursor's own
|
|
111
|
+
* guard performs for a rewritten log.
|
|
112
|
+
*/
|
|
113
|
+
export function advanceOwnedSessionEventCursor(owned, sessionId, fold, events, logFrom) {
|
|
114
|
+
const cursor = owned !== undefined && owned.sessionId === sessionId ? owned.cursor : initSessionEventCursor(fold, logFrom);
|
|
115
|
+
return { sessionId, cursor: advanceSessionEventCursor(cursor, events, logFrom, logFrom + events.length, fold) };
|
|
116
|
+
}
|