@gaunt-sloth/core 2.0.0-alpha.29 → 2.0.0-alpha.30

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.
Files changed (76) hide show
  1. package/dist/config/schema.d.ts +48 -48
  2. package/dist/config/schema.js +55 -29
  3. package/dist/config/schema.js.map +1 -1
  4. package/dist/config/shell-policy.d.ts +241 -26
  5. package/dist/config/shell-policy.js +353 -52
  6. package/dist/config/shell-policy.js.map +1 -1
  7. package/dist/config/tool-descriptions.d.ts +78 -32
  8. package/dist/config/tool-descriptions.js +72 -29
  9. package/dist/config/tool-descriptions.js.map +1 -1
  10. package/dist/config/types.d.ts +9 -4
  11. package/dist/config/types.js.map +1 -1
  12. package/dist/core/GthAbstractAgent.d.ts +13 -5
  13. package/dist/core/GthAbstractAgent.js +42 -13
  14. package/dist/core/GthAbstractAgent.js.map +1 -1
  15. package/dist/core/GthAgentRunner.d.ts +203 -9
  16. package/dist/core/GthAgentRunner.js +646 -39
  17. package/dist/core/GthAgentRunner.js.map +1 -1
  18. package/dist/core/GthLangChainAgent.js +58 -17
  19. package/dist/core/GthLangChainAgent.js.map +1 -1
  20. package/dist/core/reasoningBlocks.d.ts +60 -0
  21. package/dist/core/reasoningBlocks.js +98 -0
  22. package/dist/core/reasoningBlocks.js.map +1 -0
  23. package/dist/core/refusal.js +6 -2
  24. package/dist/core/refusal.js.map +1 -1
  25. package/dist/core/shell/approvalCapture.d.ts +271 -0
  26. package/dist/core/shell/approvalCapture.js +108 -0
  27. package/dist/core/shell/approvalCapture.js.map +1 -0
  28. package/dist/core/shell/approvalStop.d.ts +16 -7
  29. package/dist/core/shell/approvalStop.js +18 -7
  30. package/dist/core/shell/approvalStop.js.map +1 -1
  31. package/dist/core/shell/escalationSeverity.d.ts +141 -0
  32. package/dist/core/shell/escalationSeverity.js +89 -0
  33. package/dist/core/shell/escalationSeverity.js.map +1 -0
  34. package/dist/core/shell/framing.d.ts +190 -0
  35. package/dist/core/shell/framing.js +633 -0
  36. package/dist/core/shell/framing.js.map +1 -0
  37. package/dist/core/shell/hardline.d.ts +81 -0
  38. package/dist/core/shell/hardline.js +607 -0
  39. package/dist/core/shell/hardline.js.map +1 -0
  40. package/dist/core/shell/negotiation.d.ts +249 -0
  41. package/dist/core/shell/negotiation.js +355 -0
  42. package/dist/core/shell/negotiation.js.map +1 -0
  43. package/dist/core/shell/normalize.d.ts +5 -5
  44. package/dist/core/shell/normalize.js +5 -5
  45. package/dist/core/shell/rater.d.ts +353 -18
  46. package/dist/core/shell/rater.js +556 -39
  47. package/dist/core/shell/rater.js.map +1 -1
  48. package/dist/core/shell/rejection.d.ts +1 -1
  49. package/dist/core/types.d.ts +97 -4
  50. package/dist/providers/geminiThinking.d.ts +52 -0
  51. package/dist/providers/geminiThinking.js +72 -0
  52. package/dist/providers/geminiThinking.js.map +1 -0
  53. package/dist/providers/google-genai.js +4 -1
  54. package/dist/providers/google-genai.js.map +1 -1
  55. package/dist/providers/vertexai.js +4 -1
  56. package/dist/providers/vertexai.js.map +1 -1
  57. package/dist/runtime/askStructured.d.ts +45 -3
  58. package/dist/runtime/askStructured.js +46 -5
  59. package/dist/runtime/askStructured.js.map +1 -1
  60. package/dist/runtime/conversation.d.ts +2 -1
  61. package/dist/runtime/conversation.js +2 -1
  62. package/dist/runtime/conversation.js.map +1 -1
  63. package/dist/runtime/singleShot.d.ts +3 -2
  64. package/dist/runtime/singleShot.js +3 -2
  65. package/dist/runtime/singleShot.js.map +1 -1
  66. package/dist/utils/debugDump.d.ts +19 -0
  67. package/dist/utils/debugDump.js +11 -0
  68. package/dist/utils/debugDump.js.map +1 -1
  69. package/dist/utils/displayWidth.d.ts +23 -0
  70. package/dist/utils/displayWidth.js +61 -6
  71. package/dist/utils/displayWidth.js.map +1 -1
  72. package/dist/utils/systemPromptNotes.d.ts +12 -1
  73. package/dist/utils/systemPromptNotes.js +24 -2
  74. package/dist/utils/systemPromptNotes.js.map +1 -1
  75. package/package.json +1 -1
  76. package/schema/gsloth-config.schema.json +48 -48
@@ -14,33 +14,49 @@
14
14
  *
15
15
  * 1. **A tool that is auto-approved gets NO suffix.** The absence of the sentence is what marks it
16
16
  * free. Appending "this tool is already approved" would be the opposite of the design.
17
- * 2. **The suffix is derived from the rung in force AND from the set of tools the gate actually
17
+ * 2. **The suffix is derived from the rung in force AND from the set of tools that rung actually
18
18
  * gates.** "A description that disagrees with what the gate will actually do is worse than no
19
- * description at all" (§4.5), so {@link isGrantedAtRung} takes the gated set as a parameter and
20
- * both backends pass the SAME set they wire into the approval interrupt. The two can therefore
21
- * not drift: widening the gate ([[EXT-30]]) widens the descriptions in the same breath.
22
- *
23
- * **Scope boundary, per §4.3:** *"The first implementation and the measurement corpus cover the
24
- * shell only. Every other tool is granted or escalated by the rung without a rating call until
25
- * EXT-30 widens the gate."* Today `run_shell_command` is the only tool either backend wires into
26
- * the approval interrupt, so it is the only tool that can ever require approval every other tool
27
- * is **granted**, and a granted tool gets no suffix. The rung-vs-access-class rule below
28
- * (read tools granted from `read-only` up, write tools from `write` up) is implemented in full and
29
- * unit-tested against an injected wider gated set, so EXT-30 inherits it rather than re-deriving
30
- * it; it simply has no observable effect while the gated set is the shell alone.
19
+ * description at all" (§4.5), so {@link isGrantedAtRung} takes the LIVE gated set as a parameter
20
+ * and both backends pass the SAME `resolveGatedToolNames` result. The two can therefore not
21
+ * drift: widening what a rung gates ([[EXT-30]]) widens the descriptions in the same breath.
22
+ * That set is narrower than the rung-independent one wired into the interrupt
23
+ * (`resolveInterruptToolNames`), because a call the live rung does not gate is approved on
24
+ * arrival and so is genuinely free.
25
+ *
26
+ * **Scope, per §4.3.** The *rater* covers the shell only: every other gated tool goes to the human
27
+ * without a rating call until [[EXT-30]] widens the rater. The *gate* is wider than the rater. At
28
+ * the two deterministic rungs (`manual`, `write`) both backends gate every bound tool the rung's
29
+ * access class does not auto-grant the write built-ins, the shell, MCP tools (whatever their
30
+ * `readOnlyHint` says) and custom tools so at those rungs a non-granted call always reaches the
31
+ * human. At `assisted`, `auto` and `bypass` the gated set is the shell alone.
32
+ *
33
+ * `config/shell-policy.ts`'s `isToolGatedAtRung` is the single rule for all of that, and both the
34
+ * live gated set and this module's grant are projections of it, so what a rung gates and the grant
35
+ * reported here cannot disagree.
31
36
  */
32
37
  import type { ApprovalRung } from '#src/config/shell-policy.js';
33
38
  /**
34
39
  * §4.5's table, **verbatim**. The wordings are normative copy the model reads; do not paraphrase,
35
40
  * re-punctuate or "improve" them.
36
41
  *
37
- * - `read-only` and `write` share one sentence: at both rungs a non-granted call goes to the human,
42
+ * - `manual` and `write` share one sentence: at both rungs a non-granted call goes to the human,
38
43
  * so the user's approval is a certainty, not a possibility.
39
- * - `auto-safe` softens `will` to `MAY`: the rater approves what it rates safe, so only some calls
44
+ * - `assisted` softens `will` to `MAY`: the rater approves what it rates safe, so only some calls
40
45
  * reach the user.
41
- * - `full-auto` gets its **own** wording because the consequence differs the user is not asked
42
- * there, so promising the user's approval would be false. What can happen is a refusal by the
43
- * rater.
46
+ * - `auto` has its own sentence, and the difference it states is real: [[EXT-29]]'s negotiation
47
+ * hands a call the rater will not clear back to the **model**, with the reason, so the first
48
+ * consequence of an unsafe-looking call there is a refusal it can answer rather than a person
49
+ * being asked — what changes is who answers first, and that is the one thing a model choosing a
50
+ * tool can act on. A shared wording would hide it in the model's own tool-selection input.
51
+ *
52
+ * Its `MAY` therefore sits **outside** the contrast the three bullets above draw: it qualifies
53
+ * *"be refused by the auto-rater"*, not *"require the user's approval"*, so this sentence says
54
+ * nothing either way about whether a person can be asked. That silence is deliberate — the
55
+ * string is §4.5's row verbatim — and its cost is recorded here rather than left to be
56
+ * discovered: `catastrophic` escalates to a human on the FIRST hit with no rounds at all, so a
57
+ * person *can* be reached at `auto` and this input does not say so. Silent is not false, which
58
+ * is why the wording stands and why the premise quoted above still holds; a wording claiming the
59
+ * user is never asked would be false.
44
60
  * - `bypass` appends nothing to anything: no gate, so no sentence could be true.
45
61
  */
46
62
  export declare const RUNG_TOOL_DESCRIPTION_SUFFIXES: Readonly<Record<ApprovalRung, string | null>>;
@@ -57,11 +73,15 @@ export type BuiltInToolAccess = 'read' | 'write';
57
73
  * two sets overlap on `read_file`/`write_file`/`edit_file`, which is exactly why one flat table
58
74
  * keyed by name serves both.
59
75
  *
60
- * Deliberately absent: `run_shell_command`, the fixed dev-command tools, `gth_web_fetch`,
61
- * `gth_checklist`, `gth_status_update`, `show_a2ui_surface`, MCP/custom/A2A tools. None of them is
62
- * "reading or writing files in the working folder", so none is granted by a rung's access class.
63
- * (They are still *granted today* the gate does not gate them which {@link isGrantedAtRung}
64
- * decides on the gated set, not on this table.)
76
+ * Deliberately absent: `run_shell_command`, deepagents' `execute`, the fixed dev-command tools,
77
+ * `gth_web_fetch`, `gth_checklist`, `gth_status_update`, `show_a2ui_surface`, MCP/custom/A2A tools.
78
+ * None of them is "reading or writing files in the working folder", so none is granted by a rung's
79
+ * access class, and at `manual`/`write` every one of them escalates to the human.
80
+ *
81
+ * **An MCP tool's own `readOnlyHint` earns it nothing here.** A server's self-declared annotation is
82
+ * least earned exactly where the ladder is strictest, so it must not skip the prompt at those two
83
+ * rungs; membership of this table is the only thing that does. Adding a name here grants it at a
84
+ * rung, so add one only for a tool that genuinely reads or writes files in the working folder.
65
85
  */
66
86
  export declare const BUILT_IN_TOOL_ACCESS: Readonly<Record<string, BuiltInToolAccess>>;
67
87
  /** A built-in tool the rater may offer as an already-granted alternative (§4.4). */
@@ -99,23 +119,49 @@ export interface GrantedToolSummary {
99
119
  export declare const BUILT_IN_TOOL_SUMMARIES: Readonly<Record<string, string>>;
100
120
  /** The sentence §4.5 appends at this rung, or `null` when the rung appends nothing (`bypass`). */
101
121
  export declare function getRungToolDescriptionSuffix(rung: ApprovalRung): string | null;
122
+ /**
123
+ * §2 — does `rung`'s own grant cover this tool's **access class**, i.e. is the tool free *on its
124
+ * class alone*, before anything is asked about whether the gate gates it?
125
+ *
126
+ * - `read` — granted at every rung (§2.1).
127
+ * - `write` — granted from `write` up (§2.2, and §2.3/§2.4 which grant "everything `write` grants"),
128
+ * so it escalates at `manual`.
129
+ * - **no class at all** — the shell, deepagents' `execute`, a network call, an MCP tool, a custom or
130
+ * agent-authored tool: granted by no rung. There is no implicit exemption; a tool is free here
131
+ * only by appearing in {@link BUILT_IN_TOOL_ACCESS}.
132
+ *
133
+ * **This is the one implementation of that rule.** {@link isGrantedAtRung} decides a grant with it,
134
+ * and `config/shell-policy.ts`'s `isToolGatedAtRung` decides gated-set membership with it — which
135
+ * is in turn what the backends' interrupt set and `GthAgentRunner`'s own live-rung check are built
136
+ * from. So the gate cannot escalate a call the descriptions and the rater's granted list call free
137
+ * — the drift §4.5 names as worse than having no description at all. Two derivations of one rule is
138
+ * exactly what this function exists to prevent; do not inline it back into either caller.
139
+ *
140
+ * `bypass` is not special-cased: it grants everything for a reason unrelated to access class (the
141
+ * gate is off), which {@link isGrantedAtRung} states where that belongs.
142
+ */
143
+ export declare function isAccessClassGrantedAtRung(toolName: string, rung: ApprovalRung): boolean;
102
144
  /**
103
145
  * Is `toolName` auto-approved (granted, free, no prompt and no rating) at `rung`?
104
146
  *
105
147
  * @param toolName The registered tool name.
106
148
  * @param rung The rung in force for the session.
107
- * @param gatedTools The names the gate actually wires into the approval interrupt. **This is the
108
- * parameter that keeps the descriptions honest**: a tool the gate does not gate cannot require
109
- * approval, whatever a rung's table row says about tool classes, so it is granted. Both backends
110
- * pass the same set they hand to `interruptOn` / `humanInTheLoopMiddleware`.
149
+ * @param gatedTools The LIVE gated set what `rung` actually gates, i.e.
150
+ * `resolveGatedToolNames` for the rung in force. **This is the parameter that keeps the
151
+ * descriptions honest**: a tool the rung does not gate cannot require approval, whatever a rung's
152
+ * table row says about tool classes, so it is granted. It is deliberately NOT the wider,
153
+ * rung-independent set the backends wire into `interruptOn` (`resolveInterruptToolNames`): a call
154
+ * the live rung does not gate is auto-approved the moment it arrives at the runner, so describing
155
+ * it as needing approval would be a promise nothing keeps.
111
156
  *
112
157
  * Order:
113
158
  * 1. `bypass` grants everything (§2.5) — the gate is off.
114
- * 2. A tool the gate does not gate is granted at every rung (§4.3's scope boundary).
115
- * 3. A gated tool is granted only where the rung's own grant covers its access class: read tools
116
- * from `read-only` up (§2.1), write tools from `write` up (§2.2, and §2.3/§2.4 which grant
117
- * "everything `write` grants"). A gated tool with no access class the shell, a network call,
118
- * an MCP tool is granted at no rung but `bypass`.
159
+ * 2. A tool the live rung does not gate cannot require approval, so it is granted at every rung.
160
+ * 3. A gated tool is granted only where the rung's own grant covers its access class
161
+ * {@link isAccessClassGrantedAtRung}, the same rule `resolveGatedToolNames` selects the gated set
162
+ * with. At the two deterministic rungs those two uses are complementary by construction: a tool
163
+ * is gated there precisely when this returns false, so step 3 answers false for every tool step 2
164
+ * let through, and the gate and the description say the same thing.
119
165
  */
120
166
  export declare function isGrantedAtRung(toolName: string, rung: ApprovalRung, gatedTools: readonly string[]): boolean;
121
167
  /**
@@ -2,24 +2,35 @@
2
2
  * §4.5's table, **verbatim**. The wordings are normative copy the model reads; do not paraphrase,
3
3
  * re-punctuate or "improve" them.
4
4
  *
5
- * - `read-only` and `write` share one sentence: at both rungs a non-granted call goes to the human,
5
+ * - `manual` and `write` share one sentence: at both rungs a non-granted call goes to the human,
6
6
  * so the user's approval is a certainty, not a possibility.
7
- * - `auto-safe` softens `will` to `MAY`: the rater approves what it rates safe, so only some calls
7
+ * - `assisted` softens `will` to `MAY`: the rater approves what it rates safe, so only some calls
8
8
  * reach the user.
9
- * - `full-auto` gets its **own** wording because the consequence differs the user is not asked
10
- * there, so promising the user's approval would be false. What can happen is a refusal by the
11
- * rater.
9
+ * - `auto` has its own sentence, and the difference it states is real: [[EXT-29]]'s negotiation
10
+ * hands a call the rater will not clear back to the **model**, with the reason, so the first
11
+ * consequence of an unsafe-looking call there is a refusal it can answer rather than a person
12
+ * being asked — what changes is who answers first, and that is the one thing a model choosing a
13
+ * tool can act on. A shared wording would hide it in the model's own tool-selection input.
14
+ *
15
+ * Its `MAY` therefore sits **outside** the contrast the three bullets above draw: it qualifies
16
+ * *"be refused by the auto-rater"*, not *"require the user's approval"*, so this sentence says
17
+ * nothing either way about whether a person can be asked. That silence is deliberate — the
18
+ * string is §4.5's row verbatim — and its cost is recorded here rather than left to be
19
+ * discovered: `catastrophic` escalates to a human on the FIRST hit with no rounds at all, so a
20
+ * person *can* be reached at `auto` and this input does not say so. Silent is not false, which
21
+ * is why the wording stands and why the premise quoted above still holds; a wording claiming the
22
+ * user is never asked would be false.
12
23
  * - `bypass` appends nothing to anything: no gate, so no sentence could be true.
13
24
  */
14
25
  export const RUNG_TOOL_DESCRIPTION_SUFFIXES = {
15
- 'read-only': "Calling this tool will require the user's approval. Only use it when the result cannot be " +
26
+ manual: "Calling this tool will require the user's approval. Only use it when the result cannot be " +
16
27
  'achieved with the other provided tools.',
17
28
  write: "Calling this tool will require the user's approval. Only use it when the result cannot be " +
18
29
  'achieved with the other provided tools.',
19
- 'auto-safe': "Calling this tool MAY require the user's approval if it does not look safe. Only use it when " +
30
+ assisted: "Calling this tool MAY require the user's approval if it does not look safe. Only use it when " +
31
+ 'it is impossible to achieve the result with the other provided tools.',
32
+ auto: 'Calling this tool MAY be refused by the auto-rater if it does not look safe. Only use it when ' +
20
33
  'it is impossible to achieve the result with the other provided tools.',
21
- 'full-auto': 'Calling this tool MAY be refused by the auto-rater if it does not look safe. Only use it ' +
22
- 'when it is impossible to achieve the result with the other provided tools.',
23
34
  bypass: null,
24
35
  };
25
36
  /** The distinct suffix strings, for {@link stripRungToolDescriptionSuffix}'s idempotency check. */
@@ -30,11 +41,15 @@ const ALL_SUFFIXES = Array.from(new Set(Object.values(RUNG_TOOL_DESCRIPTION_SUFF
30
41
  * two sets overlap on `read_file`/`write_file`/`edit_file`, which is exactly why one flat table
31
42
  * keyed by name serves both.
32
43
  *
33
- * Deliberately absent: `run_shell_command`, the fixed dev-command tools, `gth_web_fetch`,
34
- * `gth_checklist`, `gth_status_update`, `show_a2ui_surface`, MCP/custom/A2A tools. None of them is
35
- * "reading or writing files in the working folder", so none is granted by a rung's access class.
36
- * (They are still *granted today* the gate does not gate them which {@link isGrantedAtRung}
37
- * decides on the gated set, not on this table.)
44
+ * Deliberately absent: `run_shell_command`, deepagents' `execute`, the fixed dev-command tools,
45
+ * `gth_web_fetch`, `gth_checklist`, `gth_status_update`, `show_a2ui_surface`, MCP/custom/A2A tools.
46
+ * None of them is "reading or writing files in the working folder", so none is granted by a rung's
47
+ * access class, and at `manual`/`write` every one of them escalates to the human.
48
+ *
49
+ * **An MCP tool's own `readOnlyHint` earns it nothing here.** A server's self-declared annotation is
50
+ * least earned exactly where the ladder is strictest, so it must not skip the prompt at those two
51
+ * rungs; membership of this table is the only thing that does. Adding a name here grants it at a
52
+ * rung, so add one only for a tool that genuinely reads or writes files in the working folder.
38
53
  */
39
54
  export const BUILT_IN_TOOL_ACCESS = {
40
55
  // gsloth GthFileSystemToolkit (lean backend)
@@ -114,35 +129,63 @@ export const BUILT_IN_TOOL_SUMMARIES = {
114
129
  export function getRungToolDescriptionSuffix(rung) {
115
130
  return RUNG_TOOL_DESCRIPTION_SUFFIXES[rung] ?? null;
116
131
  }
132
+ /**
133
+ * §2 — does `rung`'s own grant cover this tool's **access class**, i.e. is the tool free *on its
134
+ * class alone*, before anything is asked about whether the gate gates it?
135
+ *
136
+ * - `read` — granted at every rung (§2.1).
137
+ * - `write` — granted from `write` up (§2.2, and §2.3/§2.4 which grant "everything `write` grants"),
138
+ * so it escalates at `manual`.
139
+ * - **no class at all** — the shell, deepagents' `execute`, a network call, an MCP tool, a custom or
140
+ * agent-authored tool: granted by no rung. There is no implicit exemption; a tool is free here
141
+ * only by appearing in {@link BUILT_IN_TOOL_ACCESS}.
142
+ *
143
+ * **This is the one implementation of that rule.** {@link isGrantedAtRung} decides a grant with it,
144
+ * and `config/shell-policy.ts`'s `isToolGatedAtRung` decides gated-set membership with it — which
145
+ * is in turn what the backends' interrupt set and `GthAgentRunner`'s own live-rung check are built
146
+ * from. So the gate cannot escalate a call the descriptions and the rater's granted list call free
147
+ * — the drift §4.5 names as worse than having no description at all. Two derivations of one rule is
148
+ * exactly what this function exists to prevent; do not inline it back into either caller.
149
+ *
150
+ * `bypass` is not special-cased: it grants everything for a reason unrelated to access class (the
151
+ * gate is off), which {@link isGrantedAtRung} states where that belongs.
152
+ */
153
+ export function isAccessClassGrantedAtRung(toolName, rung) {
154
+ const access = BUILT_IN_TOOL_ACCESS[toolName];
155
+ if (access === 'read')
156
+ return true;
157
+ if (access === 'write')
158
+ return rung !== 'manual';
159
+ return false;
160
+ }
117
161
  /**
118
162
  * Is `toolName` auto-approved (granted, free, no prompt and no rating) at `rung`?
119
163
  *
120
164
  * @param toolName The registered tool name.
121
165
  * @param rung The rung in force for the session.
122
- * @param gatedTools The names the gate actually wires into the approval interrupt. **This is the
123
- * parameter that keeps the descriptions honest**: a tool the gate does not gate cannot require
124
- * approval, whatever a rung's table row says about tool classes, so it is granted. Both backends
125
- * pass the same set they hand to `interruptOn` / `humanInTheLoopMiddleware`.
166
+ * @param gatedTools The LIVE gated set what `rung` actually gates, i.e.
167
+ * `resolveGatedToolNames` for the rung in force. **This is the parameter that keeps the
168
+ * descriptions honest**: a tool the rung does not gate cannot require approval, whatever a rung's
169
+ * table row says about tool classes, so it is granted. It is deliberately NOT the wider,
170
+ * rung-independent set the backends wire into `interruptOn` (`resolveInterruptToolNames`): a call
171
+ * the live rung does not gate is auto-approved the moment it arrives at the runner, so describing
172
+ * it as needing approval would be a promise nothing keeps.
126
173
  *
127
174
  * Order:
128
175
  * 1. `bypass` grants everything (§2.5) — the gate is off.
129
- * 2. A tool the gate does not gate is granted at every rung (§4.3's scope boundary).
130
- * 3. A gated tool is granted only where the rung's own grant covers its access class: read tools
131
- * from `read-only` up (§2.1), write tools from `write` up (§2.2, and §2.3/§2.4 which grant
132
- * "everything `write` grants"). A gated tool with no access class the shell, a network call,
133
- * an MCP tool is granted at no rung but `bypass`.
176
+ * 2. A tool the live rung does not gate cannot require approval, so it is granted at every rung.
177
+ * 3. A gated tool is granted only where the rung's own grant covers its access class
178
+ * {@link isAccessClassGrantedAtRung}, the same rule `resolveGatedToolNames` selects the gated set
179
+ * with. At the two deterministic rungs those two uses are complementary by construction: a tool
180
+ * is gated there precisely when this returns false, so step 3 answers false for every tool step 2
181
+ * let through, and the gate and the description say the same thing.
134
182
  */
135
183
  export function isGrantedAtRung(toolName, rung, gatedTools) {
136
184
  if (rung === 'bypass')
137
185
  return true;
138
186
  if (!gatedTools.includes(toolName))
139
187
  return true;
140
- const access = BUILT_IN_TOOL_ACCESS[toolName];
141
- if (access === 'read')
142
- return true;
143
- if (access === 'write')
144
- return rung !== 'read-only';
145
- return false;
188
+ return isAccessClassGrantedAtRung(toolName, rung);
146
189
  }
147
190
  /**
148
191
  * Remove a previously-appended §4.5 suffix (any rung's), returning the tool's own description.
@@ -1 +1 @@
1
- {"version":3,"file":"tool-descriptions.js","sourceRoot":"","sources":["../../src/config/tool-descriptions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tool-descriptions.js","sourceRoot":"","sources":["../../src/config/tool-descriptions.ts"],"names":[],"mappings":"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"}
@@ -109,7 +109,12 @@ export interface GthConfig {
109
109
  * summarization, tool-result offload). Selecting it emits a warning. It can exhibit
110
110
  * path-divergence and sporadic failures and carries extra internal workarounds; prefer `lean`.
111
111
  *
112
- * Honored everywhere; the ACP server is still structurally deep-only and always runs deep.
112
+ * **Command-scoped**, and `docs/configuration/profiles.md` carries the one list of which commands
113
+ * honor it — don't restate it here, or the copies drift. What the type itself must say: `review`
114
+ * and `pr` — including the `pr` change-requirements discovery agent — always run lean, because
115
+ * `@gaunt-sloth/review` cannot reach the deep backend; a run that asks for `deep` there says so
116
+ * rather than dropping the key silently. The ACP server is the mirror image: structurally
117
+ * deep-only, it always runs deep and rejects `lean` outright.
113
118
  */
114
119
  agent?: {
115
120
  backend?: 'deep' | 'lean';
@@ -194,8 +199,8 @@ export interface GthConfig {
194
199
  */
195
200
  builtInTools?: BuiltInToolsSetting;
196
201
  /**
197
- * CFG-27 — the tool-approval **ladder**: one of the five rungs (`read-only` · `write` ·
198
- * `auto-safe` · `full-auto` · `bypass`), written either as the bare rung name or as an object
202
+ * CFG-27 — the tool-approval **ladder**: one of the five rungs (`manual` · `write` ·
203
+ * `assisted` · `auto` · `bypass`), written either as the bare rung name or as an object
199
204
  * carrying the rater's identity profile and the declared allow/deny lists. Each rung fully
200
205
  * determines behaviour — there are no severity thresholds, no strictness levels and no
201
206
  * independent rater switch.
@@ -206,7 +211,7 @@ export interface GthConfig {
206
211
  * discard the root's prohibitions; `allow` is REPLACED when the command states its own and
207
212
  * inherited when it does not, so a scope may narrow what runs unprompted and may never widen
208
213
  * what is prohibited (§3.1: a too-broad allow entry runs unrated, a missed deny entry does not).
209
- * Absent = `auto-safe`, resolved by `resolveApprovals`.
214
+ * Absent = `assisted`, resolved by `resolveApprovals`.
210
215
  */
211
216
  approvals?: ApprovalsConfig;
212
217
  tools?: StructuredToolInterface[] | BaseToolkit[] | ServerTool[];
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AA+rBA,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,UAAU;IACV,WAAW;IACX,MAAM;IACN,UAAU;IACV,QAAQ;IACR,cAAc;IACd,KAAK;IACL,YAAY;IACZ,aAAa;IACb,QAAQ;CACA,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/config/types.ts"],"names":[],"mappings":"AAosBA,MAAM,CAAC,MAAM,uBAAuB,GAAG;IACrC,UAAU;IACV,WAAW;IACX,MAAM;IACN,UAAU;IACV,QAAQ;IACR,cAAc;IACd,KAAK;IACL,YAAY;IACZ,aAAa;IACb,QAAQ;CACA,CAAC"}
@@ -98,11 +98,19 @@ export declare abstract class GthAbstractAgent implements GthAgentInterface {
98
98
  * enters the approvals stack, and it enters as a claim: nothing here decides whether it is
99
99
  * believed.
100
100
  *
101
- * `gatedTools` MUST be the same set the caller wires into the approval interrupt
102
- * (`humanInTheLoopMiddleware`'s `interruptOn` on lean, deepagents' `interruptOn` on deep). That
103
- * shared parameter is what makes it impossible for a description to promise an approval the gate
104
- * will not ask for §4.5's "a description that disagrees with what the gate will actually do is
105
- * worse than no description at all".
101
+ * `gatedTools` MUST be the **LIVE gated set for the rung in force** `resolveGatedToolNames` for
102
+ * that rung and NOT the set the caller wires into the approval interrupt. The two are different
103
+ * on purpose: the interrupt is installed once, at agent init, and is deliberately
104
+ * rung-independent (`resolveInterruptToolNames`, the union over every rung) so that
105
+ * `/approvals <mode>` can move the mode underneath it for the rest of the session. Passing that
106
+ * wider set here would describe tools as needing approval that the live mode does not gate — and
107
+ * a call the live mode does not gate is auto-approved the moment it reaches the runner, so the
108
+ * sentence would be a promise nothing keeps.
109
+ *
110
+ * What keeps a description from promising an approval the gate will not ask for is therefore that
111
+ * both this and `GthAgentRunner`'s own check are projections of the SAME rule,
112
+ * `isToolGatedAtRung`, evaluated against the SAME live mode — §4.5's "a description that disagrees
113
+ * with what the gate will actually do is worse than no description at all".
106
114
  *
107
115
  * `additionalToolNames` covers tools the graph builder registers itself and that therefore never
108
116
  * appear in `tools` — deepagents' own filesystem tools on the deep backend. Their descriptions
@@ -11,6 +11,7 @@ import { IterableReadableStream } from '@langchain/core/utils/stream';
11
11
  import { interrupt, Command, GraphInterrupt } from '@langchain/langgraph';
12
12
  import { extractInlineBinaryBlocks, materializeBinaryOutputs, renderAssistantContent, } from '#src/utils/binaryOutputUtils.js';
13
13
  import { detectRefusal, buildRefusalMessage } from '#src/core/refusal.js';
14
+ import { answerTextOf, segmentAssistantContent, stripReasoningBlocks, } from '#src/core/reasoningBlocks.js';
14
15
  const THINK_OPEN = '<think>';
15
16
  const THINK_CLOSE = '</think>';
16
17
  /**
@@ -216,11 +217,19 @@ export class GthAbstractAgent {
216
217
  * enters the approvals stack, and it enters as a claim: nothing here decides whether it is
217
218
  * believed.
218
219
  *
219
- * `gatedTools` MUST be the same set the caller wires into the approval interrupt
220
- * (`humanInTheLoopMiddleware`'s `interruptOn` on lean, deepagents' `interruptOn` on deep). That
221
- * shared parameter is what makes it impossible for a description to promise an approval the gate
222
- * will not ask for §4.5's "a description that disagrees with what the gate will actually do is
223
- * worse than no description at all".
220
+ * `gatedTools` MUST be the **LIVE gated set for the rung in force** `resolveGatedToolNames` for
221
+ * that rung and NOT the set the caller wires into the approval interrupt. The two are different
222
+ * on purpose: the interrupt is installed once, at agent init, and is deliberately
223
+ * rung-independent (`resolveInterruptToolNames`, the union over every rung) so that
224
+ * `/approvals <mode>` can move the mode underneath it for the rest of the session. Passing that
225
+ * wider set here would describe tools as needing approval that the live mode does not gate — and
226
+ * a call the live mode does not gate is auto-approved the moment it reaches the runner, so the
227
+ * sentence would be a promise nothing keeps.
228
+ *
229
+ * What keeps a description from promising an approval the gate will not ask for is therefore that
230
+ * both this and `GthAgentRunner`'s own check are projections of the SAME rule,
231
+ * `isToolGatedAtRung`, evaluated against the SAME live mode — §4.5's "a description that disagrees
232
+ * with what the gate will actually do is worse than no description at all".
224
233
  *
225
234
  * `additionalToolNames` covers tools the graph builder registers itself and that therefore never
226
235
  * appear in `tools` — deepagents' own filesystem tools on the deep backend. Their descriptions
@@ -358,7 +367,12 @@ export class GthAbstractAgent {
358
367
  if (refusal) {
359
368
  return this.surfaceRefusal(refusal);
360
369
  }
361
- const finalContent = finalMessage?.content;
370
+ // CFG-33: Gemini's thought summaries ride inside `content` as `thought: true` text blocks,
371
+ // which renderAssistantContent would print as part of the answer (and write to the output
372
+ // file). The plain surface has never shown reasoning — every other provider's arrives
373
+ // out-of-band in additional_kwargs — so drop them for rendering only; graph state keeps the
374
+ // message whole so the thought parts still replay as history.
375
+ const finalContent = stripReasoningBlocks(finalMessage?.content);
362
376
  const processedContent = !this.config.writeBinaryOutputsToFile
363
377
  ? {
364
378
  renderedContent: renderAssistantContent(finalContent),
@@ -524,7 +538,9 @@ export class GthAbstractAgent {
524
538
  // completed call when its ToolMessage arrives; a no-op for plain text chunks).
525
539
  toolIndication.observe(chunk);
526
540
  if (AIMessage.isInstance(chunk)) {
527
- const text = chunk.text ?? '';
541
+ // CFG-33: the ANSWER text only. `.text` folds Gemini's `thought: true` blocks into the
542
+ // answer, which would print the model's thinking inline here and in the output file.
543
+ const text = answerTextOf(chunk.content);
528
544
  totalChunks++;
529
545
  if (text.length > 0) {
530
546
  statusUpdate(StatusLevel.STREAM, text);
@@ -763,6 +779,20 @@ export class GthAbstractAgent {
763
779
  }
764
780
  }
765
781
  }
782
+ // CFG-33 — emit a message's content segments in order. A segment already classified as the
783
+ // model's thinking (a Gemini `thought: true` block) goes straight to the reasoning channel;
784
+ // answer text still passes through the TUI-C22 think splitter, so an inline `<think>` tag is
785
+ // peeled exactly as before. With no reasoning block this is the previous `.text` behaviour.
786
+ function* emitContentSegments(segments) {
787
+ for (const segment of segments) {
788
+ if (segment.kind === 'reasoning') {
789
+ yield* emitSegments([segment]);
790
+ }
791
+ else {
792
+ yield* emitSegments(thinkSplitter.push(segment.text));
793
+ }
794
+ }
795
+ }
766
796
  function* flushAggregated() {
767
797
  if (!aggregatedAIChunk)
768
798
  return;
@@ -812,9 +842,10 @@ export class GthAbstractAgent {
812
842
  // which is cumulative. TUI-C22 routes it through the think splitter so inline
813
843
  // <think>...</think> (buffered across chunks) is peeled into the reasoning channel and
814
844
  // stripped from the answer; text with no think tags passes straight through unchanged.
815
- if (chunk.text) {
816
- yield* emitSegments(thinkSplitter.push(chunk.text));
817
- }
845
+ // CFG-33 classifies the chunk's content blocks first, in order, so Gemini's `thought: true`
846
+ // blocks reach the reasoning channel instead of the answer; answer text still goes through
847
+ // the think splitter, thought text does not (it is already classified).
848
+ yield* emitContentSegments(segmentAssistantContent(chunk.content));
818
849
  }
819
850
  else if (AIMessage.isInstance(chunk)) {
820
851
  // Reasoning on a non-chunk AIMessage — a non-streamed / resumed thinking message
@@ -834,9 +865,7 @@ export class GthAbstractAgent {
834
865
  });
835
866
  aggregatedAIChunk = aggregatedAIChunk ? aggregatedAIChunk.concat(synthetic) : synthetic;
836
867
  }
837
- if (chunk.text) {
838
- yield* emitSegments(thinkSplitter.push(chunk.text));
839
- }
868
+ yield* emitContentSegments(segmentAssistantContent(chunk.content));
840
869
  // A non-chunk AIMessage is a COMPLETE message, not a delta — drain any residual now
841
870
  // (an unterminated <think> becomes reasoning, a dangling partial becomes answer) so its
842
871
  // buffered state never leaks into a subsequent message (TUI-C22).