@zfdx123/dsh-hooks-ordering 1.0.1
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/LICENSE +21 -0
- package/README.md +257 -0
- package/client.js +619 -0
- package/cordis.patch.yml +38 -0
- package/lib/dag-Bqx-sl71.d.ts +55 -0
- package/lib/dag-Bqx-sl71.d.ts.map +1 -0
- package/lib/dag-DVhoBjBG.js +48 -0
- package/lib/dag-DVhoBjBG.js.map +1 -0
- package/lib/dag.d.ts +3 -0
- package/lib/dag.js +3 -0
- package/lib/index.d.ts +121 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +190 -0
- package/lib/index.js.map +1 -0
- package/lib/serial-Cu7usHjI.js +65 -0
- package/lib/serial-Cu7usHjI.js.map +1 -0
- package/lib/serial-D8ZJCBKL.d.ts +55 -0
- package/lib/serial-D8ZJCBKL.d.ts.map +1 -0
- package/lib/serial.d.ts +5 -0
- package/lib/serial.js +6 -0
- package/lib/service-base-CCmIBwnB.d.ts +100 -0
- package/lib/service-base-CCmIBwnB.d.ts.map +1 -0
- package/lib/service-base-a5vKg62S.js +139 -0
- package/lib/service-base-a5vKg62S.js.map +1 -0
- package/lib/service-base.d.ts +4 -0
- package/lib/service-base.js +5 -0
- package/lib/topo-sort-BZ1fFcTs.d.ts +54 -0
- package/lib/topo-sort-BZ1fFcTs.d.ts.map +1 -0
- package/lib/topo-sort-CfwYPY4U.js +83 -0
- package/lib/topo-sort-CfwYPY4U.js.map +1 -0
- package/lib/topo-sort.d.ts +2 -0
- package/lib/topo-sort.js +3 -0
- package/lib/waterfall-Bu6m9gYc.js +83 -0
- package/lib/waterfall-Bu6m9gYc.js.map +1 -0
- package/lib/waterfall-_5HkptkS.d.ts +87 -0
- package/lib/waterfall-_5HkptkS.d.ts.map +1 -0
- package/lib/waterfall.d.ts +5 -0
- package/lib/waterfall.js +6 -0
- package/package.json +117 -0
- package/src/dag.ts +92 -0
- package/src/dsh.ts +181 -0
- package/src/index.ts +54 -0
- package/src/serial.ts +108 -0
- package/src/service-base.ts +179 -0
- package/src/settings.ts +97 -0
- package/src/topo-sort.ts +118 -0
- package/src/waterfall.ts +153 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { n as Orderable } from "./topo-sort-BZ1fFcTs.js";
|
|
2
|
+
|
|
3
|
+
//#region src/dag.d.ts
|
|
4
|
+
|
|
5
|
+
/** One `(hook, phase)` slice of the constraint graph to render. */
|
|
6
|
+
interface DagSection {
|
|
7
|
+
/** The controlled hook event name. */
|
|
8
|
+
readonly hook: string;
|
|
9
|
+
/** The phase within the hook (e.g. `front`/`back`). */
|
|
10
|
+
readonly phase: string;
|
|
11
|
+
/** The participants and their `before`/`after` constraints. */
|
|
12
|
+
readonly entries: readonly Orderable[];
|
|
13
|
+
}
|
|
14
|
+
/** A directed ordering relation: `from` runs before `to`. */
|
|
15
|
+
interface DagEdge {
|
|
16
|
+
/** The participant that runs first. */
|
|
17
|
+
readonly from: string;
|
|
18
|
+
/** The participant that runs after `from`. */
|
|
19
|
+
readonly to: string;
|
|
20
|
+
}
|
|
21
|
+
/** The constraint graph of one `(hook, phase)` slice. */
|
|
22
|
+
interface DagSectionGraph {
|
|
23
|
+
/** The controlled hook event name. */
|
|
24
|
+
readonly hook: string;
|
|
25
|
+
/** The phase within the hook. */
|
|
26
|
+
readonly phase: string;
|
|
27
|
+
/** Every participant name, including ones with no constraint edges. */
|
|
28
|
+
readonly nodes: readonly string[];
|
|
29
|
+
/** The ordering relations; `from` runs before `to`. Deduplicated. */
|
|
30
|
+
readonly edges: readonly DagEdge[];
|
|
31
|
+
}
|
|
32
|
+
/** The whole constraint graph: one entry per controlled `(hook, phase)`. */
|
|
33
|
+
interface Dag {
|
|
34
|
+
readonly sections: readonly DagSectionGraph[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the constraint graph of the given sections as a plain object suitable
|
|
38
|
+
* for `JSON.stringify`.
|
|
39
|
+
*
|
|
40
|
+
* Each section becomes a {@link DagSectionGraph}: `nodes` lists every entry
|
|
41
|
+
* name (constraint-free entries included), and `edges` carries the ordering
|
|
42
|
+
* relations. An edge `from -> to` reads "from runs before to": an entry's
|
|
43
|
+
* `before` target yields `{ from: entry, to: target }`, and its `after` source
|
|
44
|
+
* yields `{ from: source, to: entry }`. A reference whose peer is absent from
|
|
45
|
+
* the section imposes no edge — mirroring {@link topoSort}'s unknown-target
|
|
46
|
+
* no-op — and a relation stated from both ends (e.g. `A.before: ['B']` and
|
|
47
|
+
* `B.after: ['A']`) appears once.
|
|
48
|
+
*
|
|
49
|
+
* @param sections - the `(hook, phase)` slices to render, in output order.
|
|
50
|
+
* @returns the graph object; an empty input yields `{ sections: [] }`.
|
|
51
|
+
*/
|
|
52
|
+
declare function buildDag(sections: readonly DagSection[]): Dag;
|
|
53
|
+
//#endregion
|
|
54
|
+
export { buildDag as a, DagSectionGraph as i, DagEdge as n, DagSection as r, Dag as t };
|
|
55
|
+
//# sourceMappingURL=dag-Bqx-sl71.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dag-Bqx-sl71.d.ts","names":[],"sources":["../src/dag.ts"],"sourcesContent":[],"mappings":";;;;;UAYiB,UAAA;;;;;;6BAMY;;;UAIZ,OAAA;;;;;;;UAQA,eAAA;;;;;;;;2BAQU;;;UAIV,GAAA;8BACa;;;;;;;;;;;;;;;;;;iBAmBd,QAAA,oBAA4B,eAAe"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/dag.ts
|
|
2
|
+
/**
|
|
3
|
+
* Build the constraint graph of the given sections as a plain object suitable
|
|
4
|
+
* for `JSON.stringify`.
|
|
5
|
+
*
|
|
6
|
+
* Each section becomes a {@link DagSectionGraph}: `nodes` lists every entry
|
|
7
|
+
* name (constraint-free entries included), and `edges` carries the ordering
|
|
8
|
+
* relations. An edge `from -> to` reads "from runs before to": an entry's
|
|
9
|
+
* `before` target yields `{ from: entry, to: target }`, and its `after` source
|
|
10
|
+
* yields `{ from: source, to: entry }`. A reference whose peer is absent from
|
|
11
|
+
* the section imposes no edge — mirroring {@link topoSort}'s unknown-target
|
|
12
|
+
* no-op — and a relation stated from both ends (e.g. `A.before: ['B']` and
|
|
13
|
+
* `B.after: ['A']`) appears once.
|
|
14
|
+
*
|
|
15
|
+
* @param sections - the `(hook, phase)` slices to render, in output order.
|
|
16
|
+
* @returns the graph object; an empty input yields `{ sections: [] }`.
|
|
17
|
+
*/
|
|
18
|
+
function buildDag(sections) {
|
|
19
|
+
return { sections: sections.map((section) => {
|
|
20
|
+
const present = new Set(section.entries.map((entry) => entry.name));
|
|
21
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22
|
+
const edges = [];
|
|
23
|
+
const addEdge = (from, to) => {
|
|
24
|
+
if (!present.has(from) || !present.has(to) || from === to) return;
|
|
25
|
+
const key = `${from} -> ${to}`;
|
|
26
|
+
if (seen.has(key)) return;
|
|
27
|
+
seen.add(key);
|
|
28
|
+
edges.push({
|
|
29
|
+
from,
|
|
30
|
+
to
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
for (const entry of section.entries) {
|
|
34
|
+
for (const target of entry.before ?? []) addEdge(entry.name, target);
|
|
35
|
+
for (const source of entry.after ?? []) addEdge(source, entry.name);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
hook: section.hook,
|
|
39
|
+
phase: section.phase,
|
|
40
|
+
nodes: section.entries.map((entry) => entry.name),
|
|
41
|
+
edges
|
|
42
|
+
};
|
|
43
|
+
}) };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
export { buildDag as t };
|
|
48
|
+
//# sourceMappingURL=dag-DVhoBjBG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dag-DVhoBjBG.js","names":["edges: DagEdge[]"],"sources":["../src/dag.ts"],"sourcesContent":["/**\n * JSON rendering of the `before`/`after` constraint graph across controlled\n * hook phases. Pure and dependency-free: the output is a function of the input\n * sections alone. Deliberately NOT a topological sort — a cycle is represented\n * faithfully rather than throwing, because the graph is most useful precisely\n * when the constraints conflict and `topoSort` would fail.\n * @module dsh-hooks-ordering/dag\n */\n\nimport type { Orderable } from './topo-sort.ts'\n\n/** One `(hook, phase)` slice of the constraint graph to render. */\nexport interface DagSection {\n /** The controlled hook event name. */\n readonly hook: string\n /** The phase within the hook (e.g. `front`/`back`). */\n readonly phase: string\n /** The participants and their `before`/`after` constraints. */\n readonly entries: readonly Orderable[]\n}\n\n/** A directed ordering relation: `from` runs before `to`. */\nexport interface DagEdge {\n /** The participant that runs first. */\n readonly from: string\n /** The participant that runs after `from`. */\n readonly to: string\n}\n\n/** The constraint graph of one `(hook, phase)` slice. */\nexport interface DagSectionGraph {\n /** The controlled hook event name. */\n readonly hook: string\n /** The phase within the hook. */\n readonly phase: string\n /** Every participant name, including ones with no constraint edges. */\n readonly nodes: readonly string[]\n /** The ordering relations; `from` runs before `to`. Deduplicated. */\n readonly edges: readonly DagEdge[]\n}\n\n/** The whole constraint graph: one entry per controlled `(hook, phase)`. */\nexport interface Dag {\n readonly sections: readonly DagSectionGraph[]\n}\n\n/**\n * Build the constraint graph of the given sections as a plain object suitable\n * for `JSON.stringify`.\n *\n * Each section becomes a {@link DagSectionGraph}: `nodes` lists every entry\n * name (constraint-free entries included), and `edges` carries the ordering\n * relations. An edge `from -> to` reads \"from runs before to\": an entry's\n * `before` target yields `{ from: entry, to: target }`, and its `after` source\n * yields `{ from: source, to: entry }`. A reference whose peer is absent from\n * the section imposes no edge — mirroring {@link topoSort}'s unknown-target\n * no-op — and a relation stated from both ends (e.g. `A.before: ['B']` and\n * `B.after: ['A']`) appears once.\n *\n * @param sections - the `(hook, phase)` slices to render, in output order.\n * @returns the graph object; an empty input yields `{ sections: [] }`.\n */\nexport function buildDag(sections: readonly DagSection[]): Dag {\n return {\n sections: sections.map((section) => {\n const present = new Set(section.entries.map((entry) => entry.name))\n // Collect edges keyed by \"from->to\" to drop the same relation stated from\n // both ends, preserving first-seen order.\n const seen = new Set<string>()\n const edges: DagEdge[] = []\n const addEdge = (from: string, to: string): void => {\n // Unknown endpoint: the referenced peer is not in this section, so\n // there is no relation to record; a self-reference is likewise a no-op.\n if (!present.has(from) || !present.has(to) || from === to) return\n const key = `${from} -> ${to}`\n if (seen.has(key)) return\n seen.add(key)\n edges.push({ from, to })\n }\n for (const entry of section.entries) {\n for (const target of entry.before ?? []) addEdge(entry.name, target)\n for (const source of entry.after ?? []) addEdge(source, entry.name)\n }\n return {\n hook: section.hook,\n phase: section.phase,\n nodes: section.entries.map((entry) => entry.name),\n edges,\n }\n }),\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8DA,SAAgB,SAAS,UAAsC;AAC7D,QAAO,EACL,UAAU,SAAS,KAAK,YAAY;EAClC,MAAM,UAAU,IAAI,IAAI,QAAQ,QAAQ,KAAK,UAAU,MAAM,KAAK,CAAC;EAGnE,MAAM,uBAAO,IAAI,KAAa;EAC9B,MAAMA,QAAmB,EAAE;EAC3B,MAAM,WAAW,MAAc,OAAqB;AAGlD,OAAI,CAAC,QAAQ,IAAI,KAAK,IAAI,CAAC,QAAQ,IAAI,GAAG,IAAI,SAAS,GAAI;GAC3D,MAAM,MAAM,GAAG,KAAK,MAAM;AAC1B,OAAI,KAAK,IAAI,IAAI,CAAE;AACnB,QAAK,IAAI,IAAI;AACb,SAAM,KAAK;IAAE;IAAM;IAAI,CAAC;;AAE1B,OAAK,MAAM,SAAS,QAAQ,SAAS;AACnC,QAAK,MAAM,UAAU,MAAM,UAAU,EAAE,CAAE,SAAQ,MAAM,MAAM,OAAO;AACpE,QAAK,MAAM,UAAU,MAAM,SAAS,EAAE,CAAE,SAAQ,QAAQ,MAAM,KAAK;;AAErE,SAAO;GACL,MAAM,QAAQ;GACd,OAAO,QAAQ;GACf,OAAO,QAAQ,QAAQ,KAAK,UAAU,MAAM,KAAK;GACjD;GACD;GACD,EACH"}
|
package/lib/dag.d.ts
ADDED
package/lib/dag.js
ADDED
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { i as topoSort, n as Orderable, r as OrderingCycleError, t as DuplicateNameError } from "./topo-sort-BZ1fFcTs.js";
|
|
2
|
+
import { a as buildDag, i as DagSectionGraph, n as DagEdge, r as DagSection, t as Dag } from "./dag-Bqx-sl71.js";
|
|
3
|
+
import { a as Phase, i as HookOrderingLogConfig, n as HookControlError } from "./service-base-CCmIBwnB.js";
|
|
4
|
+
import { n as HookOrdering, r as HookOrderingConfig, t as HookEntry } from "./waterfall-_5HkptkS.js";
|
|
5
|
+
import { n as SerialHookOrdering, r as SerialHookOrderingConfig, t as SerialHookEntry } from "./serial-D8ZJCBKL.js";
|
|
6
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
7
|
+
|
|
8
|
+
//#region src/dsh.d.ts
|
|
9
|
+
|
|
10
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
11
|
+
declare const name = "hooks-ordering";
|
|
12
|
+
/**
|
|
13
|
+
* `settings` is a **hard** dependency, and it has to be declared rather than
|
|
14
|
+
* probed.
|
|
15
|
+
*
|
|
16
|
+
* cordis activates a plugin as soon as the services it declares exist, and
|
|
17
|
+
* `ctx.get('settings')` reads the service store *without* creating that
|
|
18
|
+
* requirement. Without this line the dsh layer loads in the first wave — before
|
|
19
|
+
* the host has provided `settings` — the probe returns `undefined`, and the
|
|
20
|
+
* settings namespace is never registered. Silently: no form, no error.
|
|
21
|
+
*
|
|
22
|
+
* Declaring it makes cordis wait, which also lands this plugin's prepended
|
|
23
|
+
* brackets later in the boot, exactly where the ordering guarantee wants them.
|
|
24
|
+
*
|
|
25
|
+
* This entry targets dsh only — the hook names below are dsh's, and dsh always
|
|
26
|
+
* provides `settings`, so the dependency costs nothing here. `/waterfall` and
|
|
27
|
+
* `/serial` are the entries that carry no host-service requirement.
|
|
28
|
+
*/
|
|
29
|
+
declare const inject: string[];
|
|
30
|
+
/**
|
|
31
|
+
* The dsh waterfall hooks this plugin controls by default — the ones multiple
|
|
32
|
+
* independent packages contribute to, where relative order matters. Controlling
|
|
33
|
+
* a hook with no registered participants is a no-op pass-through.
|
|
34
|
+
*
|
|
35
|
+
* Every name here was checked against the events dsh actually declares: each is
|
|
36
|
+
* a `@mode waterfall` event in the installed build, and each is dispatched with
|
|
37
|
+
* `await`, so the caller already handles a Promise and the bracket's async
|
|
38
|
+
* phases cannot change the hook's return type. `tools/code-dispatch-log` used
|
|
39
|
+
* to sit in this list and no dsh package declares it at all — controlling a
|
|
40
|
+
* name nothing dispatches "succeeds" and then does nothing forever, which is
|
|
41
|
+
* exactly the dead configuration this list must not carry.
|
|
42
|
+
*
|
|
43
|
+
* The other admission rule is {@link DEFAULT_SYNC_RETURN_HOOKS}: a hook whose
|
|
44
|
+
* value the caller consumes without awaiting cannot carry ordered participants,
|
|
45
|
+
* so controlling it by default would install a bracket nothing may ever
|
|
46
|
+
* register into — the same dead configuration by a different route.
|
|
47
|
+
*/
|
|
48
|
+
declare const DEFAULT_WATERFALL_HOOKS: readonly string[];
|
|
49
|
+
/**
|
|
50
|
+
* dsh hooks whose dispatch return value is consumed by the caller without
|
|
51
|
+
* awaiting, and which therefore cannot carry ordered participants. Verified
|
|
52
|
+
* against the installed build:
|
|
53
|
+
*
|
|
54
|
+
* - `llm/stream` — `dsh-llm` dispatches it as
|
|
55
|
+
* `return this.ctx.waterfall(this, "llm/stream", options, …)` (no await), and
|
|
56
|
+
* `dsh-session-title-llm` iterates the result with
|
|
57
|
+
* `for await (const chunk of ctx.llm.stream(options))`. A Promise there throws
|
|
58
|
+
* "not async iterable". It is a genuine multi-contributor hook (dsh-agent-loop,
|
|
59
|
+
* dsh-llm's own invariant, dsh-session-checkpoint-policy, dsh-session-title),
|
|
60
|
+
* so it is a real loss — but ordering it requires dsh to await the dispatch,
|
|
61
|
+
* not a plugin-side workaround.
|
|
62
|
+
* - `session-telemetry/record` — `dsh-session-telemetry` returns the record
|
|
63
|
+
* straight out of the waterfall and hands it to the backend, so a Promise
|
|
64
|
+
* would be emitted as a record: silent corruption, no error anywhere.
|
|
65
|
+
* - `compaction/summary-error` — `dsh-compaction-basic` dispatches it as
|
|
66
|
+
* `recover: (…) => this.ctx.waterfall(this, "compaction/summary-error", …, () => false)`
|
|
67
|
+
* (no await) and consumes the boolean synchronously in
|
|
68
|
+
* `if (!dependencies.recover(error, agent, prepared.shadowedSeqs, signal)) throw error`.
|
|
69
|
+
* A Promise is always truthy, so `!recover(…)` is always false and every
|
|
70
|
+
* summarizer failure is swallowed instead of rethrown — the compaction then
|
|
71
|
+
* proceeds as if recovery had succeeded.
|
|
72
|
+
*
|
|
73
|
+
* Override per profile with `syncReturnHooks` once the host awaits one of them.
|
|
74
|
+
*/
|
|
75
|
+
declare const DEFAULT_SYNC_RETURN_HOOKS: readonly string[];
|
|
76
|
+
/** The dsh serial hook controlled by default. */
|
|
77
|
+
declare const DEFAULT_SERIAL_HOOKS: readonly string[];
|
|
78
|
+
/** Plugin config. */
|
|
79
|
+
interface Config {
|
|
80
|
+
/**
|
|
81
|
+
* Waterfall hooks to control. Defaults to {@link DEFAULT_WATERFALL_HOOKS}.
|
|
82
|
+
* Pass `[]` to disable the waterfall service entirely.
|
|
83
|
+
*/
|
|
84
|
+
hooks?: readonly string[];
|
|
85
|
+
/**
|
|
86
|
+
* Serial hooks to control. Defaults to {@link DEFAULT_SERIAL_HOOKS}.
|
|
87
|
+
* Pass `[]` to disable the serial service entirely.
|
|
88
|
+
*/
|
|
89
|
+
serialHooks?: readonly string[];
|
|
90
|
+
/**
|
|
91
|
+
* Hooks whose return value the host consumes without awaiting, and which must
|
|
92
|
+
* therefore refuse participants. Defaults to {@link DEFAULT_SYNC_RETURN_HOOKS}.
|
|
93
|
+
* Pass `[]` — or a set without a given hook — to opt in to ordering it, once
|
|
94
|
+
* the host awaits that dispatch.
|
|
95
|
+
*/
|
|
96
|
+
syncReturnHooks?: readonly string[];
|
|
97
|
+
/** When set, the constraint DAG (JSON) is logged to this file on every change. */
|
|
98
|
+
log?: string;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Mount {@link HookOrdering} and/or {@link SerialHookOrdering} and control the
|
|
102
|
+
* configured hooks once the services are active.
|
|
103
|
+
*
|
|
104
|
+
* `config` may be `null`, and that is the ordinary case rather than an edge
|
|
105
|
+
* case: a loader row whose `config:` key is followed only by comments parses as
|
|
106
|
+
* YAML null, and a default parameter covers `undefined` but not `null`. This
|
|
107
|
+
* package's own `cordis.patch.yml` is written exactly that way — every option
|
|
108
|
+
* commented out — so a null config must mean "all defaults", not a crash.
|
|
109
|
+
*
|
|
110
|
+
* The row is the *composition* layer of the settings namespace: the Settings
|
|
111
|
+
* page edits a user layer over it, and a namespace the user has not touched
|
|
112
|
+
* resolves back to this row (or to the built-in defaults). Without a settings
|
|
113
|
+
* provider — plain Cordis — the row is the whole answer.
|
|
114
|
+
*
|
|
115
|
+
* @param ctx - the Cordis context.
|
|
116
|
+
* @param config - which hooks to control and an optional DAG `log` file; null means all defaults.
|
|
117
|
+
*/
|
|
118
|
+
declare function apply(ctx: Context, config?: Config | null): void;
|
|
119
|
+
//#endregion
|
|
120
|
+
export { type Config, DEFAULT_SERIAL_HOOKS, DEFAULT_SYNC_RETURN_HOOKS, DEFAULT_WATERFALL_HOOKS, type Dag, type DagEdge, type DagSection, type DagSectionGraph, DuplicateNameError, HookControlError, type HookEntry, HookOrdering, type HookOrderingConfig, type HookOrderingLogConfig, type Orderable, OrderingCycleError, type Phase, type SerialHookEntry, SerialHookOrdering, type SerialHookOrderingConfig, apply, buildDag, inject, name, topoSort };
|
|
121
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/dsh.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;cAuBa,IAAA;;;;;;;;;;;;;;;;;;cAmBA;;;;;;;;;;;;;;;;;;;cAoBA;;;;;;;;;;;;;;;;;;;;;;;;;;;cAuCA;;cAOA;;UAGI,MAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAwCD,KAAA,MAAW,kBAAiB"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { t as buildDag } from "./dag-DVhoBjBG.js";
|
|
2
|
+
import { n as OrderingCycleError, r as topoSort, t as DuplicateNameError } from "./topo-sort-CfwYPY4U.js";
|
|
3
|
+
import { t as HookControlError } from "./service-base-a5vKg62S.js";
|
|
4
|
+
import { n as waterfall_default, t as HookOrdering } from "./waterfall-Bu6m9gYc.js";
|
|
5
|
+
import { t as SerialHookOrdering } from "./serial-Cu7usHjI.js";
|
|
6
|
+
import Schema from "@deepseek-ai/schemastery";
|
|
7
|
+
|
|
8
|
+
//#region src/settings.ts
|
|
9
|
+
/** Namespace name — dsh requires a lowercase hyphenated identifier. */
|
|
10
|
+
const SETTINGS_NS = "hooks-ordering";
|
|
11
|
+
/**
|
|
12
|
+
* Schema for {@link SETTINGS_NS}.
|
|
13
|
+
*
|
|
14
|
+
* Every field defaults to its "off" value. The *meaningful* defaults — the
|
|
15
|
+
* hooks this plugin controls when nothing overrides them — are supplied by the
|
|
16
|
+
* caller as the composition `base` layer (see {@link resolveSettings}), so a
|
|
17
|
+
* namespace the user has never touched still resolves to a complete object.
|
|
18
|
+
*/
|
|
19
|
+
const HooksOrderingSettingsSchema = Schema.object({
|
|
20
|
+
hooks: Schema.array(Schema.string()).default([]),
|
|
21
|
+
serialHooks: Schema.array(Schema.string()).default([]),
|
|
22
|
+
log: Schema.string().default("")
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Register the settings namespace and read its resolved value.
|
|
26
|
+
*
|
|
27
|
+
* `base` is the composition layer — what the row (or the built-in defaults)
|
|
28
|
+
* asks for — so the user layer edits *over* it and a reset returns to it rather
|
|
29
|
+
* than to an empty form.
|
|
30
|
+
*
|
|
31
|
+
* `applies: 'restart'` is deliberate. Changing `hooks`/`serialHooks` means
|
|
32
|
+
* installing or removing bracket listeners on live hooks, and the coordinator
|
|
33
|
+
* has no public "release one hook" operation; a restart applies the change
|
|
34
|
+
* cleanly instead of re-wiring the dispatch chain mid-flight.
|
|
35
|
+
*
|
|
36
|
+
* @param ctx - the Cordis context to look the service up on.
|
|
37
|
+
* @param base - the composition layer to register as the base value.
|
|
38
|
+
* @returns the resolved settings, or `undefined` when the provider is missing
|
|
39
|
+
* (which `inject: ['settings']` rules out in dsh — the guard is defensive) or
|
|
40
|
+
* registration failed — the caller then uses `base`.
|
|
41
|
+
*/
|
|
42
|
+
function resolveSettings(ctx, base) {
|
|
43
|
+
const settings = ctx.get("settings");
|
|
44
|
+
if (settings === void 0) return void 0;
|
|
45
|
+
try {
|
|
46
|
+
return settings.register(SETTINGS_NS, HooksOrderingSettingsSchema, {
|
|
47
|
+
base,
|
|
48
|
+
applies: "restart"
|
|
49
|
+
}).get();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.warn("hooks-ordering: settings registration failed; falling back to the composition config:", error);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
//#region src/dsh.ts
|
|
58
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
59
|
+
const name = "hooks-ordering";
|
|
60
|
+
/**
|
|
61
|
+
* `settings` is a **hard** dependency, and it has to be declared rather than
|
|
62
|
+
* probed.
|
|
63
|
+
*
|
|
64
|
+
* cordis activates a plugin as soon as the services it declares exist, and
|
|
65
|
+
* `ctx.get('settings')` reads the service store *without* creating that
|
|
66
|
+
* requirement. Without this line the dsh layer loads in the first wave — before
|
|
67
|
+
* the host has provided `settings` — the probe returns `undefined`, and the
|
|
68
|
+
* settings namespace is never registered. Silently: no form, no error.
|
|
69
|
+
*
|
|
70
|
+
* Declaring it makes cordis wait, which also lands this plugin's prepended
|
|
71
|
+
* brackets later in the boot, exactly where the ordering guarantee wants them.
|
|
72
|
+
*
|
|
73
|
+
* This entry targets dsh only — the hook names below are dsh's, and dsh always
|
|
74
|
+
* provides `settings`, so the dependency costs nothing here. `/waterfall` and
|
|
75
|
+
* `/serial` are the entries that carry no host-service requirement.
|
|
76
|
+
*/
|
|
77
|
+
const inject = ["settings"];
|
|
78
|
+
/**
|
|
79
|
+
* The dsh waterfall hooks this plugin controls by default — the ones multiple
|
|
80
|
+
* independent packages contribute to, where relative order matters. Controlling
|
|
81
|
+
* a hook with no registered participants is a no-op pass-through.
|
|
82
|
+
*
|
|
83
|
+
* Every name here was checked against the events dsh actually declares: each is
|
|
84
|
+
* a `@mode waterfall` event in the installed build, and each is dispatched with
|
|
85
|
+
* `await`, so the caller already handles a Promise and the bracket's async
|
|
86
|
+
* phases cannot change the hook's return type. `tools/code-dispatch-log` used
|
|
87
|
+
* to sit in this list and no dsh package declares it at all — controlling a
|
|
88
|
+
* name nothing dispatches "succeeds" and then does nothing forever, which is
|
|
89
|
+
* exactly the dead configuration this list must not carry.
|
|
90
|
+
*
|
|
91
|
+
* The other admission rule is {@link DEFAULT_SYNC_RETURN_HOOKS}: a hook whose
|
|
92
|
+
* value the caller consumes without awaiting cannot carry ordered participants,
|
|
93
|
+
* so controlling it by default would install a bracket nothing may ever
|
|
94
|
+
* register into — the same dead configuration by a different route.
|
|
95
|
+
*/
|
|
96
|
+
const DEFAULT_WATERFALL_HOOKS = [
|
|
97
|
+
"agent/pre-step",
|
|
98
|
+
"agent/request",
|
|
99
|
+
"agent/request-error",
|
|
100
|
+
"system-prompt/assemble",
|
|
101
|
+
"tools/pre-execute",
|
|
102
|
+
"tools/execute",
|
|
103
|
+
"tools/post-execute",
|
|
104
|
+
"fs/write-intent",
|
|
105
|
+
"fs/edit-intent",
|
|
106
|
+
"approval/request"
|
|
107
|
+
];
|
|
108
|
+
/**
|
|
109
|
+
* dsh hooks whose dispatch return value is consumed by the caller without
|
|
110
|
+
* awaiting, and which therefore cannot carry ordered participants. Verified
|
|
111
|
+
* against the installed build:
|
|
112
|
+
*
|
|
113
|
+
* - `llm/stream` — `dsh-llm` dispatches it as
|
|
114
|
+
* `return this.ctx.waterfall(this, "llm/stream", options, …)` (no await), and
|
|
115
|
+
* `dsh-session-title-llm` iterates the result with
|
|
116
|
+
* `for await (const chunk of ctx.llm.stream(options))`. A Promise there throws
|
|
117
|
+
* "not async iterable". It is a genuine multi-contributor hook (dsh-agent-loop,
|
|
118
|
+
* dsh-llm's own invariant, dsh-session-checkpoint-policy, dsh-session-title),
|
|
119
|
+
* so it is a real loss — but ordering it requires dsh to await the dispatch,
|
|
120
|
+
* not a plugin-side workaround.
|
|
121
|
+
* - `session-telemetry/record` — `dsh-session-telemetry` returns the record
|
|
122
|
+
* straight out of the waterfall and hands it to the backend, so a Promise
|
|
123
|
+
* would be emitted as a record: silent corruption, no error anywhere.
|
|
124
|
+
* - `compaction/summary-error` — `dsh-compaction-basic` dispatches it as
|
|
125
|
+
* `recover: (…) => this.ctx.waterfall(this, "compaction/summary-error", …, () => false)`
|
|
126
|
+
* (no await) and consumes the boolean synchronously in
|
|
127
|
+
* `if (!dependencies.recover(error, agent, prepared.shadowedSeqs, signal)) throw error`.
|
|
128
|
+
* A Promise is always truthy, so `!recover(…)` is always false and every
|
|
129
|
+
* summarizer failure is swallowed instead of rethrown — the compaction then
|
|
130
|
+
* proceeds as if recovery had succeeded.
|
|
131
|
+
*
|
|
132
|
+
* Override per profile with `syncReturnHooks` once the host awaits one of them.
|
|
133
|
+
*/
|
|
134
|
+
const DEFAULT_SYNC_RETURN_HOOKS = [
|
|
135
|
+
"llm/stream",
|
|
136
|
+
"session-telemetry/record",
|
|
137
|
+
"compaction/summary-error"
|
|
138
|
+
];
|
|
139
|
+
/** The dsh serial hook controlled by default. */
|
|
140
|
+
const DEFAULT_SERIAL_HOOKS = ["agent/turn-stopping"];
|
|
141
|
+
/**
|
|
142
|
+
* Mount {@link HookOrdering} and/or {@link SerialHookOrdering} and control the
|
|
143
|
+
* configured hooks once the services are active.
|
|
144
|
+
*
|
|
145
|
+
* `config` may be `null`, and that is the ordinary case rather than an edge
|
|
146
|
+
* case: a loader row whose `config:` key is followed only by comments parses as
|
|
147
|
+
* YAML null, and a default parameter covers `undefined` but not `null`. This
|
|
148
|
+
* package's own `cordis.patch.yml` is written exactly that way — every option
|
|
149
|
+
* commented out — so a null config must mean "all defaults", not a crash.
|
|
150
|
+
*
|
|
151
|
+
* The row is the *composition* layer of the settings namespace: the Settings
|
|
152
|
+
* page edits a user layer over it, and a namespace the user has not touched
|
|
153
|
+
* resolves back to this row (or to the built-in defaults). Without a settings
|
|
154
|
+
* provider — plain Cordis — the row is the whole answer.
|
|
155
|
+
*
|
|
156
|
+
* @param ctx - the Cordis context.
|
|
157
|
+
* @param config - which hooks to control and an optional DAG `log` file; null means all defaults.
|
|
158
|
+
*/
|
|
159
|
+
function apply(ctx, config = {}) {
|
|
160
|
+
const row = config ?? {};
|
|
161
|
+
const base = {
|
|
162
|
+
hooks: row.hooks ?? DEFAULT_WATERFALL_HOOKS,
|
|
163
|
+
serialHooks: row.serialHooks ?? DEFAULT_SERIAL_HOOKS,
|
|
164
|
+
log: row.log ?? ""
|
|
165
|
+
};
|
|
166
|
+
const { hooks, serialHooks, log } = resolveSettings(ctx, base) ?? base;
|
|
167
|
+
const serviceConfig = log === "" ? {} : { log };
|
|
168
|
+
const syncReturnHooks = row.syncReturnHooks ?? DEFAULT_SYNC_RETURN_HOOKS;
|
|
169
|
+
const deps = [];
|
|
170
|
+
if (hooks.length > 0) {
|
|
171
|
+
ctx.plugin(waterfall_default, {
|
|
172
|
+
...serviceConfig,
|
|
173
|
+
syncReturnHooks
|
|
174
|
+
});
|
|
175
|
+
deps.push("hooksOrdering");
|
|
176
|
+
}
|
|
177
|
+
if (serialHooks.length > 0) {
|
|
178
|
+
ctx.plugin(SerialHookOrdering, serviceConfig);
|
|
179
|
+
deps.push("serialHooksOrdering");
|
|
180
|
+
}
|
|
181
|
+
if (deps.length === 0) return;
|
|
182
|
+
ctx.inject(deps, (ready) => {
|
|
183
|
+
for (const hook of hooks) ready.hooksOrdering.control(hook);
|
|
184
|
+
for (const hook of serialHooks) ready.serialHooksOrdering.control(hook);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//#endregion
|
|
189
|
+
export { DEFAULT_SERIAL_HOOKS, DEFAULT_SYNC_RETURN_HOOKS, DEFAULT_WATERFALL_HOOKS, DuplicateNameError, HookControlError, HookOrdering, OrderingCycleError, SerialHookOrdering, apply, buildDag, inject, name, topoSort };
|
|
190
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["DEFAULT_WATERFALL_HOOKS: readonly string[]","DEFAULT_SYNC_RETURN_HOOKS: readonly string[]","DEFAULT_SERIAL_HOOKS: readonly string[]","base: HooksOrderingSettings","deps: string[]","HookOrdering"],"sources":["../src/settings.ts","../src/dsh.ts"],"sourcesContent":["/**\n * The dsh settings namespace for this plugin: the three fields a user can edit\n * from the Settings page instead of a `cordis.patch.yml` row.\n *\n * Two constraints are load-bearing and easy to get wrong:\n *\n * - The schema handed to `settings.register` must be a **callable** schemastery\n * schema. dsh resolves a namespace by calling `schema(merged)` and reads\n * `schema.toJSON()` for the form, so a plain object throws\n * `schema is not a function` — during plugin assembly, which takes the whole\n * profile down rather than failing one form.\n * - That is also why registration is wrapped in `try/catch` here: an optional\n * settings form must never be able to stop the harness from booting.\n *\n * @module dsh-hooks-ordering/settings\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport Schema from '@deepseek-ai/schemastery'\n\n/** The three fields this plugin exposes for configuration. */\nexport interface HooksOrderingSettings {\n /** Waterfall hooks to control; `[]` disables the waterfall service entirely. */\n readonly hooks: readonly string[]\n /** Serial hooks to control; `[]` disables the serial service entirely. */\n readonly serialHooks: readonly string[]\n /** Constraint-DAG log file; the empty string means \"do not log\". */\n readonly log: string\n}\n\n/** Namespace name — dsh requires a lowercase hyphenated identifier. */\nexport const SETTINGS_NS = 'hooks-ordering'\n\n/**\n * Schema for {@link SETTINGS_NS}.\n *\n * Every field defaults to its \"off\" value. The *meaningful* defaults — the\n * hooks this plugin controls when nothing overrides them — are supplied by the\n * caller as the composition `base` layer (see {@link resolveSettings}), so a\n * namespace the user has never touched still resolves to a complete object.\n */\nexport const HooksOrderingSettingsSchema = Schema.object({\n hooks: Schema.array(Schema.string()).default([]),\n serialHooks: Schema.array(Schema.string()).default([]),\n log: Schema.string().default(''),\n})\n\n/**\n * The slice of dsh's `settings` service this plugin uses.\n *\n * Structural rather than imported from `@deepseek-ai/dsh-settings`: it describes\n * exactly the contract this plugin relies on without adding a build-time\n * dependency on a dsh package, whose internals are still 0.1.x-rc.\n */\nexport interface SettingsServiceLike {\n /**\n * @param ns - the namespace to register; must be a lowercase hyphenated identifier.\n * @param schema - a callable schemastery schema, not a plain object.\n * @param options - `base` is the composition layer, `applies` says whether an\n * edit takes effect live or needs a restart.\n * @returns a scope whose `get()` is the resolved value (schema defaults, then `base`, then the user layer).\n */\n register(\n ns: string,\n schema: unknown,\n options?: { readonly base?: unknown; readonly applies?: 'live' | 'restart' },\n ): { get(): HooksOrderingSettings }\n}\n\n/**\n * Register the settings namespace and read its resolved value.\n *\n * `base` is the composition layer — what the row (or the built-in defaults)\n * asks for — so the user layer edits *over* it and a reset returns to it rather\n * than to an empty form.\n *\n * `applies: 'restart'` is deliberate. Changing `hooks`/`serialHooks` means\n * installing or removing bracket listeners on live hooks, and the coordinator\n * has no public \"release one hook\" operation; a restart applies the change\n * cleanly instead of re-wiring the dispatch chain mid-flight.\n *\n * @param ctx - the Cordis context to look the service up on.\n * @param base - the composition layer to register as the base value.\n * @returns the resolved settings, or `undefined` when the provider is missing\n * (which `inject: ['settings']` rules out in dsh — the guard is defensive) or\n * registration failed — the caller then uses `base`.\n */\nexport function resolveSettings(ctx: Context, base: HooksOrderingSettings): HooksOrderingSettings | undefined {\n const settings = ctx.get('settings') as SettingsServiceLike | undefined\n if (settings === undefined) return undefined\n try {\n return settings.register(SETTINGS_NS, HooksOrderingSettingsSchema, { base, applies: 'restart' }).get()\n } catch (error) {\n console.warn('hooks-ordering: settings registration failed; falling back to the composition config:', error)\n return undefined\n }\n}\n","/**\n * The DeepSeek-Harness layer: a dsh plugin that mounts the ordering services\n * and takes control of the real dsh hooks that multiple independent packages\n * contribute to, so a profile can opt into deterministic ordering with one row.\n *\n * dsh (deepseek-harness) ships waterfall hooks such as `agent/pre-step`\n * (subscribed by a dozen+ independent packages), `tools/post-execute`,\n * `llm/stream`, and `system-prompt/assemble`, plus the serial hook\n * `agent/turn-stopping`. Their relative listener order is load-bearing yet\n * today decided only by binary `prepend` and registration timing. This plugin\n * controls those hooks up front; controlling an empty hook is a transparent\n * pass-through, so nothing changes until participants register with\n * `before`/`after`.\n *\n * @module dsh-hooks-ordering/dsh\n */\n\nimport type { Context } from '@deepseek-ai/cordis'\nimport HookOrdering from './waterfall.ts'\nimport { SerialHookOrdering } from './serial.ts'\nimport { type HooksOrderingSettings, resolveSettings } from './settings.ts'\n\n/** Cordis plugin name used by loader diagnostics. */\nexport const name = 'hooks-ordering'\n\n/**\n * `settings` is a **hard** dependency, and it has to be declared rather than\n * probed.\n *\n * cordis activates a plugin as soon as the services it declares exist, and\n * `ctx.get('settings')` reads the service store *without* creating that\n * requirement. Without this line the dsh layer loads in the first wave — before\n * the host has provided `settings` — the probe returns `undefined`, and the\n * settings namespace is never registered. Silently: no form, no error.\n *\n * Declaring it makes cordis wait, which also lands this plugin's prepended\n * brackets later in the boot, exactly where the ordering guarantee wants them.\n *\n * This entry targets dsh only — the hook names below are dsh's, and dsh always\n * provides `settings`, so the dependency costs nothing here. `/waterfall` and\n * `/serial` are the entries that carry no host-service requirement.\n */\nexport const inject = ['settings']\n\n/**\n * The dsh waterfall hooks this plugin controls by default — the ones multiple\n * independent packages contribute to, where relative order matters. Controlling\n * a hook with no registered participants is a no-op pass-through.\n *\n * Every name here was checked against the events dsh actually declares: each is\n * a `@mode waterfall` event in the installed build, and each is dispatched with\n * `await`, so the caller already handles a Promise and the bracket's async\n * phases cannot change the hook's return type. `tools/code-dispatch-log` used\n * to sit in this list and no dsh package declares it at all — controlling a\n * name nothing dispatches \"succeeds\" and then does nothing forever, which is\n * exactly the dead configuration this list must not carry.\n *\n * The other admission rule is {@link DEFAULT_SYNC_RETURN_HOOKS}: a hook whose\n * value the caller consumes without awaiting cannot carry ordered participants,\n * so controlling it by default would install a bracket nothing may ever\n * register into — the same dead configuration by a different route.\n */\nexport const DEFAULT_WATERFALL_HOOKS: readonly string[] = [\n 'agent/pre-step',\n 'agent/request',\n 'agent/request-error',\n 'system-prompt/assemble',\n 'tools/pre-execute',\n 'tools/execute',\n 'tools/post-execute',\n 'fs/write-intent',\n 'fs/edit-intent',\n 'approval/request',\n]\n\n/**\n * dsh hooks whose dispatch return value is consumed by the caller without\n * awaiting, and which therefore cannot carry ordered participants. Verified\n * against the installed build:\n *\n * - `llm/stream` — `dsh-llm` dispatches it as\n * `return this.ctx.waterfall(this, \"llm/stream\", options, …)` (no await), and\n * `dsh-session-title-llm` iterates the result with\n * `for await (const chunk of ctx.llm.stream(options))`. A Promise there throws\n * \"not async iterable\". It is a genuine multi-contributor hook (dsh-agent-loop,\n * dsh-llm's own invariant, dsh-session-checkpoint-policy, dsh-session-title),\n * so it is a real loss — but ordering it requires dsh to await the dispatch,\n * not a plugin-side workaround.\n * - `session-telemetry/record` — `dsh-session-telemetry` returns the record\n * straight out of the waterfall and hands it to the backend, so a Promise\n * would be emitted as a record: silent corruption, no error anywhere.\n * - `compaction/summary-error` — `dsh-compaction-basic` dispatches it as\n * `recover: (…) => this.ctx.waterfall(this, \"compaction/summary-error\", …, () => false)`\n * (no await) and consumes the boolean synchronously in\n * `if (!dependencies.recover(error, agent, prepared.shadowedSeqs, signal)) throw error`.\n * A Promise is always truthy, so `!recover(…)` is always false and every\n * summarizer failure is swallowed instead of rethrown — the compaction then\n * proceeds as if recovery had succeeded.\n *\n * Override per profile with `syncReturnHooks` once the host awaits one of them.\n */\nexport const DEFAULT_SYNC_RETURN_HOOKS: readonly string[] = [\n 'llm/stream',\n 'session-telemetry/record',\n 'compaction/summary-error',\n]\n\n/** The dsh serial hook controlled by default. */\nexport const DEFAULT_SERIAL_HOOKS: readonly string[] = ['agent/turn-stopping']\n\n/** Plugin config. */\nexport interface Config {\n /**\n * Waterfall hooks to control. Defaults to {@link DEFAULT_WATERFALL_HOOKS}.\n * Pass `[]` to disable the waterfall service entirely.\n */\n hooks?: readonly string[]\n /**\n * Serial hooks to control. Defaults to {@link DEFAULT_SERIAL_HOOKS}.\n * Pass `[]` to disable the serial service entirely.\n */\n serialHooks?: readonly string[]\n /**\n * Hooks whose return value the host consumes without awaiting, and which must\n * therefore refuse participants. Defaults to {@link DEFAULT_SYNC_RETURN_HOOKS}.\n * Pass `[]` — or a set without a given hook — to opt in to ordering it, once\n * the host awaits that dispatch.\n */\n syncReturnHooks?: readonly string[]\n /** When set, the constraint DAG (JSON) is logged to this file on every change. */\n log?: string\n}\n\n/**\n * Mount {@link HookOrdering} and/or {@link SerialHookOrdering} and control the\n * configured hooks once the services are active.\n *\n * `config` may be `null`, and that is the ordinary case rather than an edge\n * case: a loader row whose `config:` key is followed only by comments parses as\n * YAML null, and a default parameter covers `undefined` but not `null`. This\n * package's own `cordis.patch.yml` is written exactly that way — every option\n * commented out — so a null config must mean \"all defaults\", not a crash.\n *\n * The row is the *composition* layer of the settings namespace: the Settings\n * page edits a user layer over it, and a namespace the user has not touched\n * resolves back to this row (or to the built-in defaults). Without a settings\n * provider — plain Cordis — the row is the whole answer.\n *\n * @param ctx - the Cordis context.\n * @param config - which hooks to control and an optional DAG `log` file; null means all defaults.\n */\nexport function apply(ctx: Context, config: Config | null = {}): void {\n const row = config ?? {}\n const base: HooksOrderingSettings = {\n hooks: row.hooks ?? DEFAULT_WATERFALL_HOOKS,\n serialHooks: row.serialHooks ?? DEFAULT_SERIAL_HOOKS,\n log: row.log ?? '',\n }\n const { hooks, serialHooks, log } = resolveSettings(ctx, base) ?? base\n const serviceConfig = log === '' ? {} : { log }\n // Not part of the settings namespace: the sync-return list describes the\n // HOST's dispatch, not a user preference, so it is composition (row) only.\n const syncReturnHooks = row.syncReturnHooks ?? DEFAULT_SYNC_RETURN_HOOKS\n\n const deps: string[] = []\n if (hooks.length > 0) {\n ctx.plugin(HookOrdering, { ...serviceConfig, syncReturnHooks })\n deps.push('hooksOrdering')\n }\n if (serialHooks.length > 0) {\n ctx.plugin(SerialHookOrdering, serviceConfig)\n deps.push('serialHooksOrdering')\n }\n if (deps.length === 0) return\n\n // The services activate asynchronously; control the hooks once they exist.\n ctx.inject(deps, (ready) => {\n for (const hook of hooks) ready.hooksOrdering.control(hook)\n for (const hook of serialHooks) ready.serialHooksOrdering.control(hook)\n })\n}\n"],"mappings":";;;;;;;;;AA+BA,MAAa,cAAc;;;;;;;;;AAU3B,MAAa,8BAA8B,OAAO,OAAO;CACvD,OAAO,OAAO,MAAM,OAAO,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CAChD,aAAa,OAAO,MAAM,OAAO,QAAQ,CAAC,CAAC,QAAQ,EAAE,CAAC;CACtD,KAAK,OAAO,QAAQ,CAAC,QAAQ,GAAG;CACjC,CAAC;;;;;;;;;;;;;;;;;;;AA0CF,SAAgB,gBAAgB,KAAc,MAAgE;CAC5G,MAAM,WAAW,IAAI,IAAI,WAAW;AACpC,KAAI,aAAa,OAAW,QAAO;AACnC,KAAI;AACF,SAAO,SAAS,SAAS,aAAa,6BAA6B;GAAE;GAAM,SAAS;GAAW,CAAC,CAAC,KAAK;UAC/F,OAAO;AACd,UAAQ,KAAK,yFAAyF,MAAM;AAC5G;;;;;;;ACvEJ,MAAa,OAAO;;;;;;;;;;;;;;;;;;AAmBpB,MAAa,SAAS,CAAC,WAAW;;;;;;;;;;;;;;;;;;;AAoBlC,MAAaA,0BAA6C;CACxD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BD,MAAaC,4BAA+C;CAC1D;CACA;CACA;CACD;;AAGD,MAAaC,uBAA0C,CAAC,sBAAsB;;;;;;;;;;;;;;;;;;;AA2C9E,SAAgB,MAAM,KAAc,SAAwB,EAAE,EAAQ;CACpE,MAAM,MAAM,UAAU,EAAE;CACxB,MAAMC,OAA8B;EAClC,OAAO,IAAI,SAAS;EACpB,aAAa,IAAI,eAAe;EAChC,KAAK,IAAI,OAAO;EACjB;CACD,MAAM,EAAE,OAAO,aAAa,QAAQ,gBAAgB,KAAK,KAAK,IAAI;CAClE,MAAM,gBAAgB,QAAQ,KAAK,EAAE,GAAG,EAAE,KAAK;CAG/C,MAAM,kBAAkB,IAAI,mBAAmB;CAE/C,MAAMC,OAAiB,EAAE;AACzB,KAAI,MAAM,SAAS,GAAG;AACpB,MAAI,OAAOC,mBAAc;GAAE,GAAG;GAAe;GAAiB,CAAC;AAC/D,OAAK,KAAK,gBAAgB;;AAE5B,KAAI,YAAY,SAAS,GAAG;AAC1B,MAAI,OAAO,oBAAoB,cAAc;AAC7C,OAAK,KAAK,sBAAsB;;AAElC,KAAI,KAAK,WAAW,EAAG;AAGvB,KAAI,OAAO,OAAO,UAAU;AAC1B,OAAK,MAAM,QAAQ,MAAO,OAAM,cAAc,QAAQ,KAAK;AAC3D,OAAK,MAAM,QAAQ,YAAa,OAAM,oBAAoB,QAAQ,KAAK;GACvE"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { r as topoSort } from "./topo-sort-CfwYPY4U.js";
|
|
2
|
+
import { n as HookOrderingBase } from "./service-base-a5vKg62S.js";
|
|
3
|
+
import { isBailed } from "@deepseek-ai/cordis";
|
|
4
|
+
|
|
5
|
+
//#region src/serial.ts
|
|
6
|
+
/**
|
|
7
|
+
* Coordinator service registered at `ctx.serialHooksOrdering`. Controls any
|
|
8
|
+
* number of serial hooks; each owns a prepended front coordinator, an appended
|
|
9
|
+
* back coordinator, and two ordered participant lists.
|
|
10
|
+
*/
|
|
11
|
+
var SerialHookOrdering = class extends HookOrderingBase {
|
|
12
|
+
/**
|
|
13
|
+
* @param ctx - the Cordis context to register the service in.
|
|
14
|
+
* @param config - optional `log` file for the constraint DAG (JSON); `null` means defaults.
|
|
15
|
+
*/
|
|
16
|
+
constructor(ctx, config = {}) {
|
|
17
|
+
super(ctx, "serialHooksOrdering", config);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Install the two coordinator listeners. The front one is prepended so it
|
|
21
|
+
* runs ahead of the native chain; the back one is appended so it runs after
|
|
22
|
+
* the listeners present at control time. Both are effects on this service's
|
|
23
|
+
* fiber; the returned disposer removes both.
|
|
24
|
+
*/
|
|
25
|
+
install(hook, front, back) {
|
|
26
|
+
const frontCoordinator = (...args) => runSerialPhase(front, args);
|
|
27
|
+
const backCoordinator = (...args) => runSerialPhase(back, args);
|
|
28
|
+
const removeFront = this.ctx.on(hook, frontCoordinator, { prepend: true });
|
|
29
|
+
const removeBack = this.ctx.on(hook, backCoordinator, { prepend: false });
|
|
30
|
+
return () => {
|
|
31
|
+
removeFront();
|
|
32
|
+
removeBack();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Register a participant into a controlled hook phase.
|
|
37
|
+
*
|
|
38
|
+
* @param hook - the controlled serial event name.
|
|
39
|
+
* @param phase - `front` to run ahead of the native chain, `back` best-effort last.
|
|
40
|
+
* @param entry - the participant; its `run` may return a bail value to short-circuit.
|
|
41
|
+
* @returns a disposer that unregisters this participant.
|
|
42
|
+
* @throws {HookControlError} when the hook has not been {@link control}led.
|
|
43
|
+
*/
|
|
44
|
+
register(hook, phase, entry) {
|
|
45
|
+
return this.registerEntry(hook, phase, entry);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Run one phase's participants in stable topological order, awaiting each,
|
|
50
|
+
* until one bails.
|
|
51
|
+
* @param entries - the phase's registered participants.
|
|
52
|
+
* @param payload - the hook payload passed to each `run` callback.
|
|
53
|
+
* @returns the first bail value, or `undefined` if no participant bailed (so
|
|
54
|
+
* the serial dispatch continues to the next listener).
|
|
55
|
+
*/
|
|
56
|
+
async function runSerialPhase(entries, payload) {
|
|
57
|
+
for (const entry of topoSort(entries)) {
|
|
58
|
+
const result = await entry.run(...payload);
|
|
59
|
+
if (isBailed(result)) return result;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
export { SerialHookOrdering as t };
|
|
65
|
+
//# sourceMappingURL=serial-Cu7usHjI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serial-Cu7usHjI.js","names":[],"sources":["../src/serial.ts"],"sourcesContent":["/**\n * `SerialHookOrdering` — the serial-dispatch twin of {@link HookOrdering}.\n *\n * Cordis `serial` dispatch awaits listeners in registration order until one\n * *bails* (returns a value that is not `null`/`false`/`undefined`); the bail\n * value becomes the dispatch result and the remaining listeners never run.\n * There is no `next()` continuation, so unlike waterfall there is no onion to\n * wrap. This service instead brackets the hook with TWO coordinator listeners:\n *\n * - a `prepend`ed **front** coordinator that runs its participants ahead of\n * every native listener; a bail there short-circuits the whole dispatch.\n * - an appended **back** coordinator that runs its participants best-effort\n * last (see the limits below).\n *\n * Participants register into the coordinator (not the raw hook) with\n * `before`/`after` names and are run in a stable topological order.\n *\n * @module dsh-hooks-ordering/serial\n */\n\nimport { type Context, isBailed } from '@deepseek-ai/cordis'\nimport { HookOrderingBase, type HookOrderingLogConfig, type Phase } from './service-base.ts'\nimport { type Orderable, topoSort } from './topo-sort.ts'\n\n/**\n * One ordered participant in a controlled serial hook phase. Unlike the\n * waterfall entry, `run` may RETURN a value: a bail value (anything but\n * `null`/`false`/`undefined`) short-circuits the serial dispatch.\n * @typeParam A - the hook's payload argument tuple.\n */\nexport interface SerialHookEntry<A extends readonly unknown[] = readonly unknown[]> extends Orderable {\n /** Run this participant with the hook payload. A bail return stops the chain. */\n readonly run: (...args: A) => unknown | Promise<unknown>\n}\n\n/** Configuration for the {@link SerialHookOrdering} service. */\nexport type SerialHookOrderingConfig = HookOrderingLogConfig\n\ndeclare module '@deepseek-ai/cordis' {\n interface Context {\n serialHooksOrdering: SerialHookOrdering\n }\n}\n\n/**\n * Coordinator service registered at `ctx.serialHooksOrdering`. Controls any\n * number of serial hooks; each owns a prepended front coordinator, an appended\n * back coordinator, and two ordered participant lists.\n */\nexport class SerialHookOrdering extends HookOrderingBase<SerialHookEntry> {\n /**\n * @param ctx - the Cordis context to register the service in.\n * @param config - optional `log` file for the constraint DAG (JSON); `null` means defaults.\n */\n constructor(ctx: Context, config: SerialHookOrderingConfig | null = {}) {\n super(ctx, 'serialHooksOrdering', config)\n }\n\n /**\n * Install the two coordinator listeners. The front one is prepended so it\n * runs ahead of the native chain; the back one is appended so it runs after\n * the listeners present at control time. Both are effects on this service's\n * fiber; the returned disposer removes both.\n */\n protected install(hook: string, front: SerialHookEntry[], back: SerialHookEntry[]): () => void {\n const frontCoordinator = (...args: unknown[]): Promise<unknown> => runSerialPhase(front, args)\n const backCoordinator = (...args: unknown[]): Promise<unknown> => runSerialPhase(back, args)\n const removeFront = this.ctx.on(hook as never, frontCoordinator as never, { prepend: true })\n const removeBack = this.ctx.on(hook as never, backCoordinator as never, { prepend: false })\n return () => {\n removeFront()\n removeBack()\n }\n }\n\n /**\n * Register a participant into a controlled hook phase.\n *\n * @param hook - the controlled serial event name.\n * @param phase - `front` to run ahead of the native chain, `back` best-effort last.\n * @param entry - the participant; its `run` may return a bail value to short-circuit.\n * @returns a disposer that unregisters this participant.\n * @throws {HookControlError} when the hook has not been {@link control}led.\n */\n register<A extends readonly unknown[] = readonly unknown[]>(\n hook: string,\n phase: Phase,\n entry: SerialHookEntry<A>,\n ): () => void {\n return this.registerEntry(hook, phase, entry as unknown as SerialHookEntry)\n }\n}\n\n/**\n * Run one phase's participants in stable topological order, awaiting each,\n * until one bails.\n * @param entries - the phase's registered participants.\n * @param payload - the hook payload passed to each `run` callback.\n * @returns the first bail value, or `undefined` if no participant bailed (so\n * the serial dispatch continues to the next listener).\n */\nasync function runSerialPhase(entries: readonly SerialHookEntry[], payload: readonly unknown[]): Promise<unknown> {\n for (const entry of topoSort(entries)) {\n const result = await entry.run(...payload)\n if (isBailed(result)) return result\n }\n return undefined\n}\n"],"mappings":";;;;;;;;;;AAiDA,IAAa,qBAAb,cAAwC,iBAAkC;;;;;CAKxE,YAAY,KAAc,SAA0C,EAAE,EAAE;AACtE,QAAM,KAAK,uBAAuB,OAAO;;;;;;;;CAS3C,AAAU,QAAQ,MAAc,OAA0B,MAAqC;EAC7F,MAAM,oBAAoB,GAAG,SAAsC,eAAe,OAAO,KAAK;EAC9F,MAAM,mBAAmB,GAAG,SAAsC,eAAe,MAAM,KAAK;EAC5F,MAAM,cAAc,KAAK,IAAI,GAAG,MAAe,kBAA2B,EAAE,SAAS,MAAM,CAAC;EAC5F,MAAM,aAAa,KAAK,IAAI,GAAG,MAAe,iBAA0B,EAAE,SAAS,OAAO,CAAC;AAC3F,eAAa;AACX,gBAAa;AACb,eAAY;;;;;;;;;;;;CAahB,SACE,MACA,OACA,OACY;AACZ,SAAO,KAAK,cAAc,MAAM,OAAO,MAAoC;;;;;;;;;;;AAY/E,eAAe,eAAe,SAAqC,SAA+C;AAChH,MAAK,MAAM,SAAS,SAAS,QAAQ,EAAE;EACrC,MAAM,SAAS,MAAM,MAAM,IAAI,GAAG,QAAQ;AAC1C,MAAI,SAAS,OAAO,CAAE,QAAO"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { n as Orderable } from "./topo-sort-BZ1fFcTs.js";
|
|
2
|
+
import { a as Phase, i as HookOrderingLogConfig, r as HookOrderingBase } from "./service-base-CCmIBwnB.js";
|
|
3
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
4
|
+
|
|
5
|
+
//#region src/serial.d.ts
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One ordered participant in a controlled serial hook phase. Unlike the
|
|
9
|
+
* waterfall entry, `run` may RETURN a value: a bail value (anything but
|
|
10
|
+
* `null`/`false`/`undefined`) short-circuits the serial dispatch.
|
|
11
|
+
* @typeParam A - the hook's payload argument tuple.
|
|
12
|
+
*/
|
|
13
|
+
interface SerialHookEntry<A extends readonly unknown[] = readonly unknown[]> extends Orderable {
|
|
14
|
+
/** Run this participant with the hook payload. A bail return stops the chain. */
|
|
15
|
+
readonly run: (...args: A) => unknown | Promise<unknown>;
|
|
16
|
+
}
|
|
17
|
+
/** Configuration for the {@link SerialHookOrdering} service. */
|
|
18
|
+
type SerialHookOrderingConfig = HookOrderingLogConfig;
|
|
19
|
+
declare module '@deepseek-ai/cordis' {
|
|
20
|
+
interface Context {
|
|
21
|
+
serialHooksOrdering: SerialHookOrdering;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Coordinator service registered at `ctx.serialHooksOrdering`. Controls any
|
|
26
|
+
* number of serial hooks; each owns a prepended front coordinator, an appended
|
|
27
|
+
* back coordinator, and two ordered participant lists.
|
|
28
|
+
*/
|
|
29
|
+
declare class SerialHookOrdering extends HookOrderingBase<SerialHookEntry> {
|
|
30
|
+
/**
|
|
31
|
+
* @param ctx - the Cordis context to register the service in.
|
|
32
|
+
* @param config - optional `log` file for the constraint DAG (JSON); `null` means defaults.
|
|
33
|
+
*/
|
|
34
|
+
constructor(ctx: Context, config?: SerialHookOrderingConfig | null);
|
|
35
|
+
/**
|
|
36
|
+
* Install the two coordinator listeners. The front one is prepended so it
|
|
37
|
+
* runs ahead of the native chain; the back one is appended so it runs after
|
|
38
|
+
* the listeners present at control time. Both are effects on this service's
|
|
39
|
+
* fiber; the returned disposer removes both.
|
|
40
|
+
*/
|
|
41
|
+
protected install(hook: string, front: SerialHookEntry[], back: SerialHookEntry[]): () => void;
|
|
42
|
+
/**
|
|
43
|
+
* Register a participant into a controlled hook phase.
|
|
44
|
+
*
|
|
45
|
+
* @param hook - the controlled serial event name.
|
|
46
|
+
* @param phase - `front` to run ahead of the native chain, `back` best-effort last.
|
|
47
|
+
* @param entry - the participant; its `run` may return a bail value to short-circuit.
|
|
48
|
+
* @returns a disposer that unregisters this participant.
|
|
49
|
+
* @throws {HookControlError} when the hook has not been {@link control}led.
|
|
50
|
+
*/
|
|
51
|
+
register<A extends readonly unknown[] = readonly unknown[]>(hook: string, phase: Phase, entry: SerialHookEntry<A>): () => void;
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
export { SerialHookOrdering as n, SerialHookOrderingConfig as r, SerialHookEntry as t };
|
|
55
|
+
//# sourceMappingURL=serial-D8ZJCBKL.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serial-D8ZJCBKL.d.ts","names":[],"sources":["../src/serial.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;UA8BiB,2EAA2E;;0BAElE,gBAAgB;;;KAI9B,wBAAA,GAA2B;;;yBAId;;;;;;;;cASZ,kBAAA,SAA2B,iBAAiB;;;;;mBAKtC,kBAAiB;;;;;;;yCAUK,yBAAyB;;;;;;;;;;mFAsBvD,cACA,gBAAgB"}
|
package/lib/serial.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import "./topo-sort-BZ1fFcTs.js";
|
|
2
|
+
import "./dag-Bqx-sl71.js";
|
|
3
|
+
import "./service-base-CCmIBwnB.js";
|
|
4
|
+
import { n as SerialHookOrdering, r as SerialHookOrderingConfig, t as SerialHookEntry } from "./serial-D8ZJCBKL.js";
|
|
5
|
+
export { SerialHookEntry, SerialHookOrdering, SerialHookOrderingConfig };
|