@hyperdrive.bot/paseo-protocol 0.3.45 → 0.3.47
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/dist/agent-filter.d.ts +248 -0
- package/dist/agent-filter.js +340 -0
- package/dist/agent-types.d.ts +17 -0
- package/dist/language.d.ts +82 -0
- package/dist/language.js +148 -0
- package/dist/messages.d.ts +127 -117
- package/dist/messages.js +24 -1
- package/dist/validation/ws-outbound-schema-metadata.d.ts +24 -24
- package/package.json +1 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE agent-filter vocabulary and predicate, shared by every surface.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this lives in `protocol`
|
|
5
|
+
*
|
|
6
|
+
* The app has had a complete filter system for months: three-state chips over
|
|
7
|
+
* status / kanban / tag / provider / origin, a three-state archived mode, a
|
|
8
|
+
* created-vs-active time field, nesting date windows plus exact ranges, and a
|
|
9
|
+
* free-text query. The MCP `list_agents` tool had five arguments, two of which
|
|
10
|
+
* did not mean what they read as. An agent driving paseo through MCP or the
|
|
11
|
+
* CLI therefore could not ask most of the questions a human can ask by
|
|
12
|
+
* clicking, which is the gap this module closes.
|
|
13
|
+
*
|
|
14
|
+
* The obvious fix -- reimplement the predicate daemon-side -- is the one this
|
|
15
|
+
* repo has already paid for three times. `screens/workspace/session-tab-filter.ts`
|
|
16
|
+
* documents the incidents: every time a dimension was added or renamed, one of
|
|
17
|
+
* the two hand-rolled copies was updated and the other went quietly inert. The
|
|
18
|
+
* chip stayed lit, the count kept moving, and the list did not change. A third
|
|
19
|
+
* copy behind an RPC would be worse, because nobody is looking at a list while
|
|
20
|
+
* it lies.
|
|
21
|
+
*
|
|
22
|
+
* So the semantics move DOWN into the shared package instead. `packages/app`
|
|
23
|
+
* re-exports these symbols from its existing `utils/*` module paths, so every
|
|
24
|
+
* app call site is unchanged and the app's own tests keep asserting against the
|
|
25
|
+
* same functions; the server imports the same predicate for `list_agents` and
|
|
26
|
+
* the CLI imports it for `agent ls`. One implementation, three surfaces.
|
|
27
|
+
*
|
|
28
|
+
* ## What is deliberately NOT here
|
|
29
|
+
*
|
|
30
|
+
* Anything that only exists because a human is pointing at it: the chip cycle
|
|
31
|
+
* (`cycleFilterValue` is kept because the app's store calls it, but no wire
|
|
32
|
+
* surface uses it), the calendar grid, the two-tap range draft reducer, sort
|
|
33
|
+
* order. Those stay in `packages/app`.
|
|
34
|
+
*/
|
|
35
|
+
import type { WorkspaceStateBucket } from "./messages.js";
|
|
36
|
+
/**
|
|
37
|
+
* Which direction a single value is filtering in.
|
|
38
|
+
*
|
|
39
|
+
* `include` is "only this", `exclude` is "everything except this", `neutral` is
|
|
40
|
+
* the value saying nothing.
|
|
41
|
+
*/
|
|
42
|
+
export type FilterValueMode = "neutral" | "include" | "exclude";
|
|
43
|
+
/**
|
|
44
|
+
* A multi-pick selection over the values of one dimension.
|
|
45
|
+
*
|
|
46
|
+
* Both halves are REQUIRED on the in-memory shape. An optional `exclude` would
|
|
47
|
+
* read `undefined` at a call site that forgot it, and `undefined` means "off",
|
|
48
|
+
* which is exactly how this system has shipped an inert filter before. Wire
|
|
49
|
+
* schemas may accept partial input, but they normalize into this before it
|
|
50
|
+
* reaches the predicate.
|
|
51
|
+
*/
|
|
52
|
+
export interface FilterSelection<T extends string = string> {
|
|
53
|
+
/** Values the reader asked to see exclusively. Empty = no inclusion rule. */
|
|
54
|
+
include: T[];
|
|
55
|
+
/** Values the reader asked to remove. Empty = no exclusion rule. */
|
|
56
|
+
exclude: T[];
|
|
57
|
+
}
|
|
58
|
+
/** The cycle a chip walks: rest -> only -> except -> rest. */
|
|
59
|
+
export declare const FILTER_VALUE_MODES: FilterValueMode[];
|
|
60
|
+
export declare function isFilterValueMode(value: unknown): value is FilterValueMode;
|
|
61
|
+
/** A selection with no value in either half: the "dimension is off" value. */
|
|
62
|
+
export declare function emptyFilterSelection<T extends string = string>(): FilterSelection<T>;
|
|
63
|
+
/** Lifts a plain inclusion array into a selection. */
|
|
64
|
+
export declare function filterSelectionFromInclude<T extends string = string>(include: readonly T[]): FilterSelection<T>;
|
|
65
|
+
/**
|
|
66
|
+
* Builds a selection from two optional halves, which is the shape every wire
|
|
67
|
+
* surface hands over. A value listed in both halves keeps its inclusion, so a
|
|
68
|
+
* malformed request narrows rather than contradicting itself.
|
|
69
|
+
*/
|
|
70
|
+
export declare function filterSelectionFrom<T extends string = string>(include: readonly T[] | undefined, exclude: readonly T[] | undefined): FilterSelection<T>;
|
|
71
|
+
/**
|
|
72
|
+
* Re-validates an untrusted value into a selection, keeping only string entries
|
|
73
|
+
* and never letting a value sit in both halves.
|
|
74
|
+
*
|
|
75
|
+
* Persisted app state is the only untrusted input the app has here, and a
|
|
76
|
+
* malformed one must degrade to "off" rather than throw on a screen that is
|
|
77
|
+
* just trying to render a list.
|
|
78
|
+
*/
|
|
79
|
+
export declare function normalizeFilterSelection<T extends string = string>(value: unknown): FilterSelection<T>;
|
|
80
|
+
/** The mode `value` currently sits in. Unknown values are neutral by construction. */
|
|
81
|
+
export declare function getFilterValueMode<T extends string>(selection: FilterSelection<T>, value: T): FilterValueMode;
|
|
82
|
+
/** The mode a chip lands on from `current`. Wraps at the end of the cycle. */
|
|
83
|
+
export declare function nextFilterValueMode(current: FilterValueMode): FilterValueMode;
|
|
84
|
+
/** Moves `value` to `mode`, returning a NEW selection. */
|
|
85
|
+
export declare function setFilterValueMode<T extends string>(selection: FilterSelection<T>, value: T, mode: FilterValueMode): FilterSelection<T>;
|
|
86
|
+
/** One chip tap: advance `value` to the next mode in the cycle. */
|
|
87
|
+
export declare function cycleFilterValue<T extends string>(selection: FilterSelection<T>, value: T): FilterSelection<T>;
|
|
88
|
+
/** Returns `value` to neutral. */
|
|
89
|
+
export declare function clearFilterValue<T extends string>(selection: FilterSelection<T>, value: T): FilterSelection<T>;
|
|
90
|
+
/** True when the dimension is narrowing the list at all. */
|
|
91
|
+
export declare function isFilterSelectionActive<T extends string>(selection: FilterSelection<T>): boolean;
|
|
92
|
+
/** Every value the reader has touched, includes first. */
|
|
93
|
+
export declare function filterSelectionEntries<T extends string>(selection: FilterSelection<T>): {
|
|
94
|
+
value: T;
|
|
95
|
+
mode: Exclude<FilterValueMode, "neutral">;
|
|
96
|
+
}[];
|
|
97
|
+
/**
|
|
98
|
+
* Whether a subject with `value` survives this dimension.
|
|
99
|
+
*
|
|
100
|
+
* `null` models a subject with no value in this dimension at all. It cannot
|
|
101
|
+
* satisfy an inclusion rule -- the reader asked for specific values and this is
|
|
102
|
+
* not one of them -- but it is not excluded either, since it matches no
|
|
103
|
+
* excluded value.
|
|
104
|
+
*/
|
|
105
|
+
export declare function passesFilterSelection<T extends string>(value: T | null, selection: FilterSelection<T>): boolean;
|
|
106
|
+
/**
|
|
107
|
+
* Whether a subject carrying MANY values in this dimension survives (tags).
|
|
108
|
+
*
|
|
109
|
+
* Include is satisfied by ANY overlap, and exclude drops the subject if ANY of
|
|
110
|
+
* its values is excluded -- "except #wip" has to remove a session tagged
|
|
111
|
+
* `#wip #urgent`, or the chip means nothing on precisely the sessions that
|
|
112
|
+
* carry several tags.
|
|
113
|
+
*/
|
|
114
|
+
export declare function passesFilterSelectionMulti<T extends string>(values: readonly T[], selection: FilterSelection<T>): boolean;
|
|
115
|
+
/**
|
|
116
|
+
* `all` shows archived alongside live, `only` shows archived exclusively,
|
|
117
|
+
* `hide` drops them.
|
|
118
|
+
*
|
|
119
|
+
* A boolean cannot express `only`, which is why the wire surfaces take this
|
|
120
|
+
* instead of the `includeArchived` flag they started with.
|
|
121
|
+
*/
|
|
122
|
+
export type AgentArchivedMode = "all" | "only" | "hide";
|
|
123
|
+
export declare const AGENT_ARCHIVED_MODES: AgentArchivedMode[];
|
|
124
|
+
export declare function isAgentArchivedMode(value: unknown): value is AgentArchivedMode;
|
|
125
|
+
export declare function nextAgentArchivedMode(current: AgentArchivedMode): AgentArchivedMode;
|
|
126
|
+
export declare function passesAgentArchivedMode(isArchived: boolean, mode: AgentArchivedMode): boolean;
|
|
127
|
+
export declare function isAgentArchivedModeActive(mode: AgentArchivedMode): boolean;
|
|
128
|
+
/** `root` = a human started it; `delegated` = another agent spawned it. */
|
|
129
|
+
export type AgentOrigin = "root" | "delegated";
|
|
130
|
+
export declare const AGENT_ORIGINS: AgentOrigin[];
|
|
131
|
+
export declare function isAgentOrigin(value: unknown): value is AgentOrigin;
|
|
132
|
+
export declare function deriveAgentOrigin(labels: Record<string, string> | null | undefined): AgentOrigin;
|
|
133
|
+
/**
|
|
134
|
+
* Which timestamp the span is measured against.
|
|
135
|
+
*
|
|
136
|
+
* This is a MODE, not a second dimension. AND-ing "created today" with "active
|
|
137
|
+
* today" produces a mostly-empty list; the reader means one question or the
|
|
138
|
+
* other.
|
|
139
|
+
*/
|
|
140
|
+
export type AgentTimeField = "created" | "active";
|
|
141
|
+
/**
|
|
142
|
+
* The rolling presets. They NEST (`today` is inside `3d` is inside `7d` ...),
|
|
143
|
+
* which is why the span is single-select: OR-ing two always collapses to the
|
|
144
|
+
* wider one, leaving the narrower one looking selected while doing nothing.
|
|
145
|
+
* `stale` is the strict complement of `30d`, so the two partition the corpus.
|
|
146
|
+
*/
|
|
147
|
+
export type AgentTimeWindow = "today" | "3d" | "7d" | "30d" | "stale";
|
|
148
|
+
export declare const AGENT_TIME_FIELDS: AgentTimeField[];
|
|
149
|
+
export declare const AGENT_TIME_WINDOWS: AgentTimeWindow[];
|
|
150
|
+
/**
|
|
151
|
+
* An inclusive span of LOCAL CALENDAR DAYS, `YYYY-MM-DD` on both ends.
|
|
152
|
+
*
|
|
153
|
+
* Stored as days rather than instants on purpose: resolving to instants at
|
|
154
|
+
* store time freezes one timezone's midnight into the filter, so the same saved
|
|
155
|
+
* span covers a different 24 hours after a flight or a DST change.
|
|
156
|
+
*/
|
|
157
|
+
export interface AgentDateRange {
|
|
158
|
+
from: string;
|
|
159
|
+
to: string;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Presets and exact dates are ONE dimension in two shapes.
|
|
163
|
+
*
|
|
164
|
+
* A second field beside the window would be able to hold "7 days" AND "March
|
|
165
|
+
* 4" at once -- an intersection that is empty unless March 4 falls inside the
|
|
166
|
+
* week, and whose emptiness no surface can explain to the reader.
|
|
167
|
+
*/
|
|
168
|
+
export type AgentTimeSelection = {
|
|
169
|
+
kind: "window";
|
|
170
|
+
window: AgentTimeWindow;
|
|
171
|
+
} | {
|
|
172
|
+
kind: "range";
|
|
173
|
+
range: AgentDateRange;
|
|
174
|
+
};
|
|
175
|
+
export declare function isAgentTimeField(value: unknown): value is AgentTimeField;
|
|
176
|
+
export declare function isAgentTimeWindow(value: unknown): value is AgentTimeWindow;
|
|
177
|
+
/** True for a real `YYYY-MM-DD` calendar day (rejects 2026-02-31). */
|
|
178
|
+
export declare function isAgentDay(value: unknown): value is string;
|
|
179
|
+
export declare function isAgentDateRange(value: unknown): value is AgentDateRange;
|
|
180
|
+
export declare function isAgentTimeSelection(value: unknown): value is AgentTimeSelection;
|
|
181
|
+
export declare function formatAgentDay(date: Date): string;
|
|
182
|
+
export declare function normalizeAgentDateRange(range: AgentDateRange): AgentDateRange;
|
|
183
|
+
export declare function isWithinAgentDateRange(timestamp: Date | null | undefined, range: AgentDateRange): boolean;
|
|
184
|
+
export declare function isWithinAgentTimeWindow(timestamp: Date | null | undefined, window: AgentTimeWindow, now: Date): boolean;
|
|
185
|
+
/** Dispatches on the selection's shape. `null` means the dimension is off. */
|
|
186
|
+
export declare function matchesAgentTimeSelection(timestamp: Date | null | undefined, selection: AgentTimeSelection | null, now: Date): boolean;
|
|
187
|
+
/**
|
|
188
|
+
* Everything one agent contributes to a filtering decision.
|
|
189
|
+
*
|
|
190
|
+
* Built by each surface from whatever record it holds (the app's directory
|
|
191
|
+
* entry, the daemon's list-item payload) so the predicate itself never has to
|
|
192
|
+
* know which one it is looking at.
|
|
193
|
+
*/
|
|
194
|
+
export interface AgentFilterSubject {
|
|
195
|
+
/** The derived state bucket. See `deriveAgentStateBucket`. */
|
|
196
|
+
stateBucket: WorkspaceStateBucket;
|
|
197
|
+
/** User tag names, without the `tag:` prefix. */
|
|
198
|
+
tags: string[];
|
|
199
|
+
/** The kanban bucket id from `labels.kanban`, or `null` when unresolved. */
|
|
200
|
+
kanbanBucket: string | null;
|
|
201
|
+
isArchived: boolean;
|
|
202
|
+
/** The timestamp the caller resolved for the active `timeField`. */
|
|
203
|
+
timeValue: Date | null;
|
|
204
|
+
/** Provider id (`claude`, `codex`, `pool-tu`, ...). */
|
|
205
|
+
provider: string;
|
|
206
|
+
origin: AgentOrigin;
|
|
207
|
+
/** Lowercased haystack for the free-text query (title + cwd). */
|
|
208
|
+
searchableText: string;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* A complete filter request.
|
|
212
|
+
*
|
|
213
|
+
* Composition: OR within a dimension, AND across dimensions.
|
|
214
|
+
*
|
|
215
|
+
* Note which dimensions carry an `exclude` half and which do not. `stateBucket`
|
|
216
|
+
* and `origin` are CLOSED sets (six values and two), so "except running" is
|
|
217
|
+
* expressible as an include list of the others and an exclude half would buy
|
|
218
|
+
* nothing. `tag`, `kanban` and `provider` are OPEN sets discovered from live
|
|
219
|
+
* data, so their complement cannot be enumerated at all and the exclude half is
|
|
220
|
+
* the only way to say "except this one". They are typed identically for the
|
|
221
|
+
* predicate's benefit; it is the WIRE schemas that decline to offer the
|
|
222
|
+
* exclude half where it adds nothing.
|
|
223
|
+
*/
|
|
224
|
+
export interface AgentFilterCriteria {
|
|
225
|
+
stateSelection: FilterSelection<WorkspaceStateBucket>;
|
|
226
|
+
tagSelection: FilterSelection<string>;
|
|
227
|
+
kanbanSelection: FilterSelection<string>;
|
|
228
|
+
archivedMode: AgentArchivedMode;
|
|
229
|
+
timeSelection: AgentTimeSelection | null;
|
|
230
|
+
providerSelection: FilterSelection<string>;
|
|
231
|
+
originSelection: FilterSelection<AgentOrigin>;
|
|
232
|
+
/** Lowercased, trimmed. Empty = no text filter. */
|
|
233
|
+
textQuery: string;
|
|
234
|
+
}
|
|
235
|
+
/** Criteria that keep everything. The starting point for a partial request. */
|
|
236
|
+
export declare function emptyAgentFilterCriteria(): AgentFilterCriteria;
|
|
237
|
+
/** True when any dimension is narrowing the corpus. */
|
|
238
|
+
export declare function isAgentFilterActive(criteria: AgentFilterCriteria): boolean;
|
|
239
|
+
/**
|
|
240
|
+
* Whether `subject` survives every dimension of `criteria`.
|
|
241
|
+
*
|
|
242
|
+
* `now` is a parameter rather than a `new Date()` call so that a caller
|
|
243
|
+
* computing facet counts and a caller filtering the list read the SAME instant.
|
|
244
|
+
* Two independent clock reads can count a boundary session in the chip and drop
|
|
245
|
+
* it from the list, producing a "showing 4 of 5" that never resolves.
|
|
246
|
+
*/
|
|
247
|
+
export declare function agentPassesFilter(subject: AgentFilterSubject, criteria: AgentFilterCriteria, now: Date): boolean;
|
|
248
|
+
//# sourceMappingURL=agent-filter.d.ts.map
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE agent-filter vocabulary and predicate, shared by every surface.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this lives in `protocol`
|
|
5
|
+
*
|
|
6
|
+
* The app has had a complete filter system for months: three-state chips over
|
|
7
|
+
* status / kanban / tag / provider / origin, a three-state archived mode, a
|
|
8
|
+
* created-vs-active time field, nesting date windows plus exact ranges, and a
|
|
9
|
+
* free-text query. The MCP `list_agents` tool had five arguments, two of which
|
|
10
|
+
* did not mean what they read as. An agent driving paseo through MCP or the
|
|
11
|
+
* CLI therefore could not ask most of the questions a human can ask by
|
|
12
|
+
* clicking, which is the gap this module closes.
|
|
13
|
+
*
|
|
14
|
+
* The obvious fix -- reimplement the predicate daemon-side -- is the one this
|
|
15
|
+
* repo has already paid for three times. `screens/workspace/session-tab-filter.ts`
|
|
16
|
+
* documents the incidents: every time a dimension was added or renamed, one of
|
|
17
|
+
* the two hand-rolled copies was updated and the other went quietly inert. The
|
|
18
|
+
* chip stayed lit, the count kept moving, and the list did not change. A third
|
|
19
|
+
* copy behind an RPC would be worse, because nobody is looking at a list while
|
|
20
|
+
* it lies.
|
|
21
|
+
*
|
|
22
|
+
* So the semantics move DOWN into the shared package instead. `packages/app`
|
|
23
|
+
* re-exports these symbols from its existing `utils/*` module paths, so every
|
|
24
|
+
* app call site is unchanged and the app's own tests keep asserting against the
|
|
25
|
+
* same functions; the server imports the same predicate for `list_agents` and
|
|
26
|
+
* the CLI imports it for `agent ls`. One implementation, three surfaces.
|
|
27
|
+
*
|
|
28
|
+
* ## What is deliberately NOT here
|
|
29
|
+
*
|
|
30
|
+
* Anything that only exists because a human is pointing at it: the chip cycle
|
|
31
|
+
* (`cycleFilterValue` is kept because the app's store calls it, but no wire
|
|
32
|
+
* surface uses it), the calendar grid, the two-tap range draft reducer, sort
|
|
33
|
+
* order. Those stay in `packages/app`.
|
|
34
|
+
*/
|
|
35
|
+
import { isDelegatedAgent } from "./agent-labels.js";
|
|
36
|
+
/** The cycle a chip walks: rest -> only -> except -> rest. */
|
|
37
|
+
export const FILTER_VALUE_MODES = ["neutral", "include", "exclude"];
|
|
38
|
+
export function isFilterValueMode(value) {
|
|
39
|
+
return value === "neutral" || value === "include" || value === "exclude";
|
|
40
|
+
}
|
|
41
|
+
/** A selection with no value in either half: the "dimension is off" value. */
|
|
42
|
+
export function emptyFilterSelection() {
|
|
43
|
+
return { include: [], exclude: [] };
|
|
44
|
+
}
|
|
45
|
+
/** Lifts a plain inclusion array into a selection. */
|
|
46
|
+
export function filterSelectionFromInclude(include) {
|
|
47
|
+
return { include: [...include], exclude: [] };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds a selection from two optional halves, which is the shape every wire
|
|
51
|
+
* surface hands over. A value listed in both halves keeps its inclusion, so a
|
|
52
|
+
* malformed request narrows rather than contradicting itself.
|
|
53
|
+
*/
|
|
54
|
+
export function filterSelectionFrom(include, exclude) {
|
|
55
|
+
const includes = dedupe(include);
|
|
56
|
+
return { include: includes, exclude: dedupe(exclude).filter((v) => !includes.includes(v)) };
|
|
57
|
+
}
|
|
58
|
+
function dedupe(values) {
|
|
59
|
+
if (!values)
|
|
60
|
+
return [];
|
|
61
|
+
const seen = new Set();
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const value of values) {
|
|
64
|
+
if (typeof value !== "string" || value === "" || seen.has(value))
|
|
65
|
+
continue;
|
|
66
|
+
seen.add(value);
|
|
67
|
+
out.push(value);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Re-validates an untrusted value into a selection, keeping only string entries
|
|
73
|
+
* and never letting a value sit in both halves.
|
|
74
|
+
*
|
|
75
|
+
* Persisted app state is the only untrusted input the app has here, and a
|
|
76
|
+
* malformed one must degrade to "off" rather than throw on a screen that is
|
|
77
|
+
* just trying to render a list.
|
|
78
|
+
*/
|
|
79
|
+
export function normalizeFilterSelection(value) {
|
|
80
|
+
if (typeof value !== "object" || value === null)
|
|
81
|
+
return emptyFilterSelection();
|
|
82
|
+
const raw = value;
|
|
83
|
+
return filterSelectionFrom(normalizeValueList(raw.include), normalizeValueList(raw.exclude));
|
|
84
|
+
}
|
|
85
|
+
function normalizeValueList(value) {
|
|
86
|
+
return Array.isArray(value) ? dedupe(value.filter((e) => typeof e === "string")) : [];
|
|
87
|
+
}
|
|
88
|
+
/** The mode `value` currently sits in. Unknown values are neutral by construction. */
|
|
89
|
+
export function getFilterValueMode(selection, value) {
|
|
90
|
+
if (selection.include.includes(value))
|
|
91
|
+
return "include";
|
|
92
|
+
if (selection.exclude.includes(value))
|
|
93
|
+
return "exclude";
|
|
94
|
+
return "neutral";
|
|
95
|
+
}
|
|
96
|
+
/** The mode a chip lands on from `current`. Wraps at the end of the cycle. */
|
|
97
|
+
export function nextFilterValueMode(current) {
|
|
98
|
+
const index = FILTER_VALUE_MODES.indexOf(current);
|
|
99
|
+
return FILTER_VALUE_MODES[(index + 1) % FILTER_VALUE_MODES.length] ?? "neutral";
|
|
100
|
+
}
|
|
101
|
+
/** Moves `value` to `mode`, returning a NEW selection. */
|
|
102
|
+
export function setFilterValueMode(selection, value, mode) {
|
|
103
|
+
const include = selection.include.filter((entry) => entry !== value);
|
|
104
|
+
const exclude = selection.exclude.filter((entry) => entry !== value);
|
|
105
|
+
switch (mode) {
|
|
106
|
+
case "include":
|
|
107
|
+
return { include: [...include, value], exclude };
|
|
108
|
+
case "exclude":
|
|
109
|
+
return { include, exclude: [...exclude, value] };
|
|
110
|
+
case "neutral":
|
|
111
|
+
return { include, exclude };
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** One chip tap: advance `value` to the next mode in the cycle. */
|
|
115
|
+
export function cycleFilterValue(selection, value) {
|
|
116
|
+
return setFilterValueMode(selection, value, nextFilterValueMode(getFilterValueMode(selection, value)));
|
|
117
|
+
}
|
|
118
|
+
/** Returns `value` to neutral. */
|
|
119
|
+
export function clearFilterValue(selection, value) {
|
|
120
|
+
return setFilterValueMode(selection, value, "neutral");
|
|
121
|
+
}
|
|
122
|
+
/** True when the dimension is narrowing the list at all. */
|
|
123
|
+
export function isFilterSelectionActive(selection) {
|
|
124
|
+
return selection.include.length > 0 || selection.exclude.length > 0;
|
|
125
|
+
}
|
|
126
|
+
/** Every value the reader has touched, includes first. */
|
|
127
|
+
export function filterSelectionEntries(selection) {
|
|
128
|
+
return [
|
|
129
|
+
...selection.include.map((value) => ({ value, mode: "include" })),
|
|
130
|
+
...selection.exclude.map((value) => ({ value, mode: "exclude" })),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Whether a subject with `value` survives this dimension.
|
|
135
|
+
*
|
|
136
|
+
* `null` models a subject with no value in this dimension at all. It cannot
|
|
137
|
+
* satisfy an inclusion rule -- the reader asked for specific values and this is
|
|
138
|
+
* not one of them -- but it is not excluded either, since it matches no
|
|
139
|
+
* excluded value.
|
|
140
|
+
*/
|
|
141
|
+
export function passesFilterSelection(value, selection) {
|
|
142
|
+
if (selection.include.length > 0 && (value === null || !selection.include.includes(value))) {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
if (value !== null && selection.exclude.includes(value))
|
|
146
|
+
return false;
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Whether a subject carrying MANY values in this dimension survives (tags).
|
|
151
|
+
*
|
|
152
|
+
* Include is satisfied by ANY overlap, and exclude drops the subject if ANY of
|
|
153
|
+
* its values is excluded -- "except #wip" has to remove a session tagged
|
|
154
|
+
* `#wip #urgent`, or the chip means nothing on precisely the sessions that
|
|
155
|
+
* carry several tags.
|
|
156
|
+
*/
|
|
157
|
+
export function passesFilterSelectionMulti(values, selection) {
|
|
158
|
+
if (selection.include.length > 0 && !selection.include.some((entry) => values.includes(entry))) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (selection.exclude.some((entry) => values.includes(entry)))
|
|
162
|
+
return false;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
export const AGENT_ARCHIVED_MODES = ["all", "only", "hide"];
|
|
166
|
+
export function isAgentArchivedMode(value) {
|
|
167
|
+
return value === "all" || value === "only" || value === "hide";
|
|
168
|
+
}
|
|
169
|
+
export function nextAgentArchivedMode(current) {
|
|
170
|
+
const index = AGENT_ARCHIVED_MODES.indexOf(current);
|
|
171
|
+
return AGENT_ARCHIVED_MODES[(index + 1) % AGENT_ARCHIVED_MODES.length] ?? "all";
|
|
172
|
+
}
|
|
173
|
+
export function passesAgentArchivedMode(isArchived, mode) {
|
|
174
|
+
switch (mode) {
|
|
175
|
+
case "only":
|
|
176
|
+
return isArchived;
|
|
177
|
+
case "hide":
|
|
178
|
+
return !isArchived;
|
|
179
|
+
case "all":
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export function isAgentArchivedModeActive(mode) {
|
|
184
|
+
return mode !== "all";
|
|
185
|
+
}
|
|
186
|
+
export const AGENT_ORIGINS = ["root", "delegated"];
|
|
187
|
+
export function isAgentOrigin(value) {
|
|
188
|
+
return value === "root" || value === "delegated";
|
|
189
|
+
}
|
|
190
|
+
export function deriveAgentOrigin(labels) {
|
|
191
|
+
return isDelegatedAgent({ labels }) ? "delegated" : "root";
|
|
192
|
+
}
|
|
193
|
+
export const AGENT_TIME_FIELDS = ["created", "active"];
|
|
194
|
+
export const AGENT_TIME_WINDOWS = ["today", "3d", "7d", "30d", "stale"];
|
|
195
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
196
|
+
const DAY_PATTERN = /^\d{4}-\d{2}-\d{2}$/;
|
|
197
|
+
export function isAgentTimeField(value) {
|
|
198
|
+
return value === "created" || value === "active";
|
|
199
|
+
}
|
|
200
|
+
export function isAgentTimeWindow(value) {
|
|
201
|
+
return (value === "today" || value === "3d" || value === "7d" || value === "30d" || value === "stale");
|
|
202
|
+
}
|
|
203
|
+
/** True for a real `YYYY-MM-DD` calendar day (rejects 2026-02-31). */
|
|
204
|
+
export function isAgentDay(value) {
|
|
205
|
+
if (typeof value !== "string" || !DAY_PATTERN.test(value))
|
|
206
|
+
return false;
|
|
207
|
+
const [year, month, day] = value.split("-").map(Number);
|
|
208
|
+
if (year === undefined || month === undefined || day === undefined)
|
|
209
|
+
return false;
|
|
210
|
+
const date = new Date(year, month - 1, day);
|
|
211
|
+
return date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day;
|
|
212
|
+
}
|
|
213
|
+
export function isAgentDateRange(value) {
|
|
214
|
+
if (typeof value !== "object" || value === null)
|
|
215
|
+
return false;
|
|
216
|
+
const candidate = value;
|
|
217
|
+
return isAgentDay(candidate.from) && isAgentDay(candidate.to);
|
|
218
|
+
}
|
|
219
|
+
export function isAgentTimeSelection(value) {
|
|
220
|
+
if (typeof value !== "object" || value === null)
|
|
221
|
+
return false;
|
|
222
|
+
const candidate = value;
|
|
223
|
+
if (candidate.kind === "window")
|
|
224
|
+
return isAgentTimeWindow(candidate.window);
|
|
225
|
+
if (candidate.kind === "range")
|
|
226
|
+
return isAgentDateRange(candidate.range);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
export function formatAgentDay(date) {
|
|
230
|
+
const year = String(date.getFullYear()).padStart(4, "0");
|
|
231
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
232
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
233
|
+
return `${year}-${month}-${day}`;
|
|
234
|
+
}
|
|
235
|
+
export function normalizeAgentDateRange(range) {
|
|
236
|
+
return range.from <= range.to ? range : { from: range.to, to: range.from };
|
|
237
|
+
}
|
|
238
|
+
function startOfLocalDay(now) {
|
|
239
|
+
const start = new Date(now);
|
|
240
|
+
start.setHours(0, 0, 0, 0);
|
|
241
|
+
return start.getTime();
|
|
242
|
+
}
|
|
243
|
+
function startOfDayMs(day) {
|
|
244
|
+
const [year, month, date] = day.split("-").map(Number);
|
|
245
|
+
return new Date(year ?? 0, (month ?? 1) - 1, date ?? 1, 0, 0, 0, 0).getTime();
|
|
246
|
+
}
|
|
247
|
+
function endOfDayExclusiveMs(day) {
|
|
248
|
+
const [year, month, date] = day.split("-").map(Number);
|
|
249
|
+
return new Date(year ?? 0, (month ?? 1) - 1, (date ?? 1) + 1, 0, 0, 0, 0).getTime();
|
|
250
|
+
}
|
|
251
|
+
export function isWithinAgentDateRange(timestamp, range) {
|
|
252
|
+
if (!timestamp)
|
|
253
|
+
return false;
|
|
254
|
+
const value = timestamp.getTime();
|
|
255
|
+
if (!Number.isFinite(value))
|
|
256
|
+
return false;
|
|
257
|
+
const { from, to } = normalizeAgentDateRange(range);
|
|
258
|
+
return value >= startOfDayMs(from) && value < endOfDayExclusiveMs(to);
|
|
259
|
+
}
|
|
260
|
+
export function isWithinAgentTimeWindow(timestamp, window, now) {
|
|
261
|
+
if (!timestamp)
|
|
262
|
+
return false;
|
|
263
|
+
const value = timestamp.getTime();
|
|
264
|
+
if (!Number.isFinite(value))
|
|
265
|
+
return false;
|
|
266
|
+
const nowMs = now.getTime();
|
|
267
|
+
switch (window) {
|
|
268
|
+
case "today":
|
|
269
|
+
return value >= startOfLocalDay(now);
|
|
270
|
+
case "3d":
|
|
271
|
+
return value >= nowMs - 3 * DAY_MS;
|
|
272
|
+
case "7d":
|
|
273
|
+
return value >= nowMs - 7 * DAY_MS;
|
|
274
|
+
case "30d":
|
|
275
|
+
return value >= nowMs - 30 * DAY_MS;
|
|
276
|
+
case "stale":
|
|
277
|
+
return value < nowMs - 30 * DAY_MS;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
/** Dispatches on the selection's shape. `null` means the dimension is off. */
|
|
281
|
+
export function matchesAgentTimeSelection(timestamp, selection, now) {
|
|
282
|
+
if (selection === null)
|
|
283
|
+
return true;
|
|
284
|
+
return selection.kind === "window"
|
|
285
|
+
? isWithinAgentTimeWindow(timestamp, selection.window, now)
|
|
286
|
+
: isWithinAgentDateRange(timestamp, selection.range);
|
|
287
|
+
}
|
|
288
|
+
/** Criteria that keep everything. The starting point for a partial request. */
|
|
289
|
+
export function emptyAgentFilterCriteria() {
|
|
290
|
+
return {
|
|
291
|
+
stateSelection: emptyFilterSelection(),
|
|
292
|
+
tagSelection: emptyFilterSelection(),
|
|
293
|
+
kanbanSelection: emptyFilterSelection(),
|
|
294
|
+
archivedMode: "all",
|
|
295
|
+
timeSelection: null,
|
|
296
|
+
providerSelection: emptyFilterSelection(),
|
|
297
|
+
originSelection: emptyFilterSelection(),
|
|
298
|
+
textQuery: "",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
/** True when any dimension is narrowing the corpus. */
|
|
302
|
+
export function isAgentFilterActive(criteria) {
|
|
303
|
+
return (isFilterSelectionActive(criteria.stateSelection) ||
|
|
304
|
+
isFilterSelectionActive(criteria.tagSelection) ||
|
|
305
|
+
isFilterSelectionActive(criteria.kanbanSelection) ||
|
|
306
|
+
isAgentArchivedModeActive(criteria.archivedMode) ||
|
|
307
|
+
criteria.timeSelection !== null ||
|
|
308
|
+
isFilterSelectionActive(criteria.providerSelection) ||
|
|
309
|
+
isFilterSelectionActive(criteria.originSelection) ||
|
|
310
|
+
criteria.textQuery.length > 0);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Whether `subject` survives every dimension of `criteria`.
|
|
314
|
+
*
|
|
315
|
+
* `now` is a parameter rather than a `new Date()` call so that a caller
|
|
316
|
+
* computing facet counts and a caller filtering the list read the SAME instant.
|
|
317
|
+
* Two independent clock reads can count a boundary session in the chip and drop
|
|
318
|
+
* it from the list, producing a "showing 4 of 5" that never resolves.
|
|
319
|
+
*/
|
|
320
|
+
export function agentPassesFilter(subject, criteria, now) {
|
|
321
|
+
if (!passesFilterSelection(subject.stateBucket, criteria.stateSelection))
|
|
322
|
+
return false;
|
|
323
|
+
if (!passesFilterSelectionMulti(subject.tags, criteria.tagSelection))
|
|
324
|
+
return false;
|
|
325
|
+
if (!passesFilterSelection(subject.kanbanBucket, criteria.kanbanSelection))
|
|
326
|
+
return false;
|
|
327
|
+
if (!passesAgentArchivedMode(subject.isArchived, criteria.archivedMode))
|
|
328
|
+
return false;
|
|
329
|
+
if (!matchesAgentTimeSelection(subject.timeValue, criteria.timeSelection, now))
|
|
330
|
+
return false;
|
|
331
|
+
if (!passesFilterSelection(subject.provider, criteria.providerSelection))
|
|
332
|
+
return false;
|
|
333
|
+
if (!passesFilterSelection(subject.origin, criteria.originSelection))
|
|
334
|
+
return false;
|
|
335
|
+
if (criteria.textQuery.length > 0 && !subject.searchableText.includes(criteria.textQuery)) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
//# sourceMappingURL=agent-filter.js.map
|
package/dist/agent-types.d.ts
CHANGED
|
@@ -429,6 +429,23 @@ export interface AgentSessionConfig {
|
|
|
429
429
|
* Mapped by each provider to its native instruction field.
|
|
430
430
|
*/
|
|
431
431
|
systemPrompt?: string;
|
|
432
|
+
/**
|
|
433
|
+
* BCP-47 tag for the language the agent must WRITE and SPEAK back in
|
|
434
|
+
* ("en-US", "pt-BR"). Session-scoped on purpose: it describes the work
|
|
435
|
+
* ("this agent works in English"), so it follows the agent across every
|
|
436
|
+
* device, client and resume, and it applies to typed input just as much as
|
|
437
|
+
* to dictated or spoken input.
|
|
438
|
+
*
|
|
439
|
+
* Deliberately a structured field and NOT prose folded into `systemPrompt`:
|
|
440
|
+
* voice mode rewrites `systemPrompt` by regex on every toggle
|
|
441
|
+
* (`stripVoiceModeSystemPrompt`), so anything stored in that string is
|
|
442
|
+
* living in a field another feature routinely edits. The instruction text is
|
|
443
|
+
* composed at prompt-build time instead.
|
|
444
|
+
*
|
|
445
|
+
* Undefined means "no instruction", i.e. the model mirrors the user's own
|
|
446
|
+
* language, which is the pre-existing behaviour.
|
|
447
|
+
*/
|
|
448
|
+
outputLanguage?: string;
|
|
432
449
|
modeId?: string;
|
|
433
450
|
model?: string;
|
|
434
451
|
thinkingOptionId?: string;
|