@mercury-fw/formatter 0.70.0 → 0.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # @mercury-fw/formatter
2
2
 
3
+ ## 0.71.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 2b08f7e: - `formatterPlugin` renders the `display` of every tool a plugin contributes, typed tools included (`{ ok: true, …, display }`)
8
+ - formatter rule `{ item, empty?, always: true }`: that kind is shown after the answer without `present`, the model gets `shown: true` next to its `displayRef`
9
+ - `ToolDisplay.always`
10
+ - the core strips `display` from every plugin tool's result and stashes it, a typed tool's too
11
+ - stashed list labels: the tool name, its input and the item count
12
+ - a failed or cancelled turn's displays are no longer appended to the next answer
13
+ - `present` is offered with any plugin tool
14
+ - **Breaking:** `createCliTool` no longer takes `stashDisplay` and returns the post-processor's `display` as it is: drop the option
15
+ - **Breaking:** a hand-written `DisplayHandler` returns `{ text, always? }` instead of a string: wrap the text
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [2b08f7e]
20
+ - @mercury-fw/plugin-types@0.71.0
21
+
3
22
  ## 0.70.0
4
23
 
5
24
  ### Patch Changes
package/README.md CHANGED
@@ -24,6 +24,8 @@ export default defineMercuryConfig({
24
24
  ```
25
25
 
26
26
  - `formatter<D>(rules)` takes one rule per kind of list, keyed by the kinds the plugin declares (`D`, e.g. `JiraDisplays`), so a kind it doesn't emit or a rule for the wrong item shape fails the typecheck. A rule is the line for each item, or `{ item, empty }` when an empty list needs a text of its own. Items are joined with a blank line. A rule also gets `t`, the texts of the person the turn is for, as its second argument (`(issue, t) => …`), and `empty` can be a function of it (`(t) => t("app.jira.no-issues")`, the key in the app's `locales/en.json` and its other language files), so an app that speaks several languages writes its lists in each person's.
27
- - `formatterPlugin(plugin, handler)` wraps a plugin so every list it hands over goes through the rules; everything else the plugin contributes passes through untouched.
27
+ - A rule in object form can also say `always: true`, for a kind that is the answer itself (a link the person asked for): Mercury shows it after the answer whether or not the model calls `present`, and tells the model it's shown so it doesn't write it again.
28
+ - `formatterPlugin(plugin, handler)` wraps a plugin so every list its tools hand over goes through the rules, a command-line tool's or one written in code alike (a tool hands one over by returning `{ ok: true, …, display: { type, items } }`); everything else the plugin contributes passes through untouched.
29
+ - A handler written by hand (instead of `formatter`) returns `{ text }`, or `{ text, always: true }`, or `undefined` when there's nothing to show.
28
30
 
29
31
  MIT
package/dist/index.d.ts CHANGED
@@ -8,20 +8,30 @@
8
8
  *
9
9
  * - `formatter(rules)` builds a `DisplayHandler` from a `kind → rule` map.
10
10
  * - `formatterPlugin(plugin, handler)` wraps a data plugin so every display
11
- * its post-processor emits goes through the handler; everything else the
12
- * plugin contributes passes through untouched.
11
+ * its tools return goes through the handler, a CLI tool's or a typed one's
12
+ * alike; everything else the plugin contributes passes through untouched.
13
13
  */
14
14
  import type { Plugin, ToolDisplay, Translate } from "@mercury-fw/plugin-types";
15
- /** Renders one display into the single text block shown to the user, in the
16
- * turn's language (`t`), or returns `undefined` when there is nothing to show
17
- * (no rule for its kind, or an empty list with no empty text). */
18
- export type DisplayHandler = (display: ToolDisplay, t: Translate) => string | undefined;
15
+ /** What a handler renders one display into: the single text block shown to the
16
+ * user, and `always` when the display is the answer itself, shown without the
17
+ * model calling `present`. */
18
+ export type RenderedDisplay = {
19
+ text: string;
20
+ always?: true;
21
+ };
22
+ /** Renders one display, in the turn's language (`t`), or returns `undefined`
23
+ * when there is nothing to show (no rule for its kind, or an empty list with no
24
+ * empty text). */
25
+ export type DisplayHandler = (display: ToolDisplay, t: Translate) => RenderedDisplay | undefined;
19
26
  /** How one kind of list is rendered: the line for each item, or that plus the
20
- * text to show when the list is empty. Both may use `t`, the texts of the
21
- * person the turn is for (an app's own keys in its language files). */
27
+ * text to show when the list is empty and whether the display is always shown
28
+ * (`always`: the answer itself, e.g. a link the person asked for, appended
29
+ * without the model calling `present`). The texts may use `t`, the texts of
30
+ * the person the turn is for (an app's own keys in its language files). */
22
31
  export type FormatterRule<T> = ((item: T, t: Translate) => string) | {
23
32
  item: (item: T, t: Translate) => string;
24
33
  empty?: string | ((t: Translate) => string);
34
+ always?: true;
25
35
  };
26
36
  /**
27
37
  * Builds a handler from the instance's rules. `D` is the data plugin's map of
@@ -33,10 +43,12 @@ export declare function formatter<D>(rules: {
33
43
  [K in keyof D]?: FormatterRule<D[K]>;
34
44
  }): DisplayHandler;
35
45
  /**
36
- * Wraps `plugin` so its post-processor's displays are rendered through
46
+ * Wraps `plugin` so the displays its tools return are rendered through
37
47
  * `handler`. Returns a plugin identical to `plugin` except for a `build` that
38
48
  * runs the inner `build` (passing the runtime context straight through) and
39
- * wraps the post-processor it contributes; the wrapped one flows to the
40
- * plugin's `sessionTools` factory, which the composition hands it to.
49
+ * wraps its `sessionTools` factory: each tool it builds keeps every field but
50
+ * `execute`, whose output goes through the handler with the turn's texts. A
51
+ * CLI tool's display (attached by its post-processor) and a typed tool's take
52
+ * the same path; the post-processor itself is left as it is.
41
53
  */
42
54
  export declare function formatterPlugin(plugin: Plugin, handler: DisplayHandler): Plugin;
package/index.ts CHANGED
@@ -8,22 +8,29 @@
8
8
  *
9
9
  * - `formatter(rules)` builds a `DisplayHandler` from a `kind → rule` map.
10
10
  * - `formatterPlugin(plugin, handler)` wraps a data plugin so every display
11
- * its post-processor emits goes through the handler; everything else the
12
- * plugin contributes passes through untouched.
11
+ * its tools return goes through the handler, a CLI tool's or a typed one's
12
+ * alike; everything else the plugin contributes passes through untouched.
13
13
  */
14
- import type { Plugin, CliPostProcessor, CliResult, ToolDisplay, Translate } from "@mercury-fw/plugin-types";
14
+ import type { ExecutableTool, Plugin, PluginRuntimeContributions, ToolDisplay, Translate } from "@mercury-fw/plugin-types";
15
15
 
16
- /** Renders one display into the single text block shown to the user, in the
17
- * turn's language (`t`), or returns `undefined` when there is nothing to show
18
- * (no rule for its kind, or an empty list with no empty text). */
19
- export type DisplayHandler = (display: ToolDisplay, t: Translate) => string | undefined;
16
+ /** What a handler renders one display into: the single text block shown to the
17
+ * user, and `always` when the display is the answer itself, shown without the
18
+ * model calling `present`. */
19
+ export type RenderedDisplay = { text: string; always?: true };
20
+
21
+ /** Renders one display, in the turn's language (`t`), or returns `undefined`
22
+ * when there is nothing to show (no rule for its kind, or an empty list with no
23
+ * empty text). */
24
+ export type DisplayHandler = (display: ToolDisplay, t: Translate) => RenderedDisplay | undefined;
20
25
 
21
26
  /** How one kind of list is rendered: the line for each item, or that plus the
22
- * text to show when the list is empty. Both may use `t`, the texts of the
23
- * person the turn is for (an app's own keys in its language files). */
27
+ * text to show when the list is empty and whether the display is always shown
28
+ * (`always`: the answer itself, e.g. a link the person asked for, appended
29
+ * without the model calling `present`). The texts may use `t`, the texts of
30
+ * the person the turn is for (an app's own keys in its language files). */
24
31
  export type FormatterRule<T> =
25
32
  | ((item: T, t: Translate) => string)
26
- | { item: (item: T, t: Translate) => string; empty?: string | ((t: Translate) => string) };
33
+ | { item: (item: T, t: Translate) => string; empty?: string | ((t: Translate) => string); always?: true };
27
34
 
28
35
  /** Items are rendered one per block, separated by a blank line. */
29
36
  const ITEM_SEPARATOR = "\n\n";
@@ -41,58 +48,96 @@ export function formatter<D>(rules: { [K in keyof D]?: FormatterRule<D[K]> }): D
41
48
  if (rule === undefined) {
42
49
  return undefined;
43
50
  }
44
- const { item, empty } = typeof rule === "function" ? { item: rule, empty: undefined } : rule;
45
- if (display.items.length === 0) {
46
- return typeof empty === "function" ? empty(t) : empty;
51
+ const { item, empty, always } = typeof rule === "function" ? { item: rule, empty: undefined, always: undefined } : rule;
52
+ const text =
53
+ display.items.length === 0
54
+ ? typeof empty === "function"
55
+ ? empty(t)
56
+ : empty
57
+ : display.items.map((i) => item(i, t)).join(ITEM_SEPARATOR);
58
+ if (text === undefined) {
59
+ return undefined;
47
60
  }
48
- return display.items.map((i) => item(i, t)).join(ITEM_SEPARATOR);
61
+ return always ? { text, always } : { text };
49
62
  };
50
63
  }
51
64
 
52
- /** Applies `handler` to an ok result's display. A rendered block replaces the
53
- * structured items; an empty list with nothing to show drops the display; a
54
- * non-empty list with no rule stays structured (so it isn't shown) and is
55
- * reported through `log`. Non-ok results and results with no display are
56
- * returned as they are, without calling the handler. */
57
- function renderDisplay(
58
- result: CliResult,
59
- handler: DisplayHandler,
60
- t: Translate,
61
- log: (m: string) => void,
62
- pluginName: string,
63
- ): CliResult {
64
- if (!result.ok || !result.display) {
65
- return result;
65
+ /** A tool's output carrying a display to render: an ok result (`ok: true`)
66
+ * with a top-level `display`. */
67
+ type DisplayOutput = Record<string, unknown> & { ok: true; display: ToolDisplay };
68
+
69
+ /** Whether `output` is an ok result with a `display` (see `DisplayOutput`). */
70
+ function hasDisplay(output: unknown): output is DisplayOutput {
71
+ if (typeof output !== "object" || output === null) return false;
72
+ const o = output as Record<string, unknown>;
73
+ const display = o.display as ToolDisplay | undefined;
74
+ return o.ok === true && typeof display === "object" && display !== null && Array.isArray(display.items);
75
+ }
76
+
77
+ /** Applies `handler` to an ok output's display. A rendered block replaces the
78
+ * structured items (marked `always` when the handler says so); an empty list
79
+ * with nothing to show drops the display; a non-empty list with no rule stays
80
+ * structured (so it isn't shown) and is reported through `log`. Any other
81
+ * output is returned as it is, without calling the handler. */
82
+ function renderDisplay(output: unknown, handler: DisplayHandler, t: Translate, log: (m: string) => void, pluginName: string): unknown {
83
+ if (!hasDisplay(output)) {
84
+ return output;
66
85
  }
67
- const text = handler(result.display, t);
68
- if (text !== undefined) {
69
- return { ...result, display: { ...result.display, items: [text] } };
86
+ const rendered = handler(output.display, t);
87
+ if (rendered !== undefined) {
88
+ const display: ToolDisplay = { type: output.display.type, items: [rendered.text] };
89
+ if (rendered.always) display.always = true;
90
+ return { ...output, display };
70
91
  }
71
- if (result.display.items.length === 0) {
72
- const { display: _dropped, ...rest } = result;
92
+ if (output.display.items.length === 0) {
93
+ const { display: _dropped, ...rest } = output;
73
94
  return rest;
74
95
  }
75
- log(`[formatter] no rule for "${result.display.type}" lists from plugin "${pluginName}" — list not shown`);
76
- return result;
96
+ log(`[formatter] no rule for "${output.display.type}" lists from plugin "${pluginName}" — list not shown`);
97
+ return output;
77
98
  }
78
99
 
79
100
  /**
80
- * Wraps `plugin` so its post-processor's displays are rendered through
101
+ * Wraps `plugin` so the displays its tools return are rendered through
81
102
  * `handler`. Returns a plugin identical to `plugin` except for a `build` that
82
103
  * runs the inner `build` (passing the runtime context straight through) and
83
- * wraps the post-processor it contributes; the wrapped one flows to the
84
- * plugin's `sessionTools` factory, which the composition hands it to.
104
+ * wraps its `sessionTools` factory: each tool it builds keeps every field but
105
+ * `execute`, whose output goes through the handler with the turn's texts. A
106
+ * CLI tool's display (attached by its post-processor) and a typed tool's take
107
+ * the same path; the post-processor itself is left as it is.
85
108
  */
86
109
  export function formatterPlugin(plugin: Plugin, handler: DisplayHandler): Plugin {
87
110
  return {
88
111
  ...plugin,
89
112
  build: (ctx) => {
90
113
  const inner = plugin.build ? plugin.build(ctx) : {};
91
- const proc = inner.postProcess;
92
- const wrapped: CliPostProcessor | undefined = proc
93
- ? (cmd, result, t) => renderDisplay(proc(cmd, result, t), handler, t, ctx.log, plugin.name)
94
- : undefined;
95
- return { ...inner, postProcess: wrapped };
114
+ const factory = inner.sessionTools;
115
+ if (factory === undefined) {
116
+ return inner;
117
+ }
118
+ const sessionTools: PluginRuntimeContributions["sessionTools"] = (sctx, postProcess) => {
119
+ const tools = factory(sctx, postProcess);
120
+ const wrapped: typeof tools = {};
121
+ for (const [name, tool] of Object.entries(tools)) {
122
+ const { execute } = tool;
123
+ wrapped[name] =
124
+ execute === undefined
125
+ ? tool
126
+ : ({
127
+ ...tool,
128
+ execute: async (input: unknown, options: unknown) =>
129
+ renderDisplay(
130
+ await (execute as (i: unknown, o: unknown) => unknown)(input, options),
131
+ handler,
132
+ sctx.t,
133
+ ctx.log,
134
+ plugin.name,
135
+ ),
136
+ } as ExecutableTool);
137
+ }
138
+ return wrapped;
139
+ };
140
+ return { ...inner, sessionTools };
96
141
  },
97
142
  };
98
143
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mercury-fw/formatter",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,7 +29,7 @@
29
29
  "typecheck": "tsc --noEmit"
30
30
  },
31
31
  "dependencies": {
32
- "@mercury-fw/plugin-types": "0.70.0"
32
+ "@mercury-fw/plugin-types": "0.71.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@mercury-fw/typescript-config": "*",