@tagma/sdk 0.7.3 → 0.7.5

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 (230) hide show
  1. package/README.md +85 -57
  2. package/dist/approval.d.ts +2 -12
  3. package/dist/approval.d.ts.map +1 -1
  4. package/dist/approval.js +1 -90
  5. package/dist/approval.js.map +1 -1
  6. package/dist/bootstrap.d.ts +1 -1
  7. package/dist/bootstrap.d.ts.map +1 -1
  8. package/dist/completions/file-exists.js +1 -1
  9. package/dist/completions/file-exists.js.map +1 -1
  10. package/dist/completions/output-check.d.ts.map +1 -1
  11. package/dist/completions/output-check.js +17 -4
  12. package/dist/completions/output-check.js.map +1 -1
  13. package/dist/config.d.ts +4 -4
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +2 -2
  16. package/dist/config.js.map +1 -1
  17. package/dist/dataflow.d.ts +3 -0
  18. package/dist/dataflow.d.ts.map +1 -0
  19. package/dist/dataflow.js +2 -0
  20. package/dist/dataflow.js.map +1 -0
  21. package/dist/drivers/opencode.d.ts.map +1 -1
  22. package/dist/drivers/opencode.js +23 -71
  23. package/dist/drivers/opencode.js.map +1 -1
  24. package/dist/engine.d.ts +5 -56
  25. package/dist/engine.d.ts.map +1 -1
  26. package/dist/engine.js +7 -297
  27. package/dist/engine.js.map +1 -1
  28. package/dist/index.d.ts +4 -6
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -4
  31. package/dist/index.js.map +1 -1
  32. package/dist/logger.d.ts +2 -60
  33. package/dist/logger.d.ts.map +1 -1
  34. package/dist/logger.js +1 -153
  35. package/dist/logger.js.map +1 -1
  36. package/dist/middlewares/static-context.d.ts.map +1 -1
  37. package/dist/middlewares/static-context.js +1 -2
  38. package/dist/middlewares/static-context.js.map +1 -1
  39. package/dist/pipeline-runner.d.ts.map +1 -1
  40. package/dist/pipeline-runner.js +2 -2
  41. package/dist/pipeline-runner.js.map +1 -1
  42. package/dist/plugins.d.ts +2 -2
  43. package/dist/plugins.d.ts.map +1 -1
  44. package/dist/plugins.js +1 -1
  45. package/dist/plugins.js.map +1 -1
  46. package/dist/runner.d.ts +1 -35
  47. package/dist/runner.d.ts.map +1 -1
  48. package/dist/runner.js +1 -610
  49. package/dist/runner.js.map +1 -1
  50. package/dist/runtime/adapters/stdin-approval.d.ts +2 -0
  51. package/dist/runtime/adapters/stdin-approval.d.ts.map +1 -0
  52. package/dist/runtime/adapters/stdin-approval.js +2 -0
  53. package/dist/runtime/adapters/stdin-approval.js.map +1 -0
  54. package/dist/runtime/adapters/websocket-approval.d.ts +2 -0
  55. package/dist/runtime/adapters/websocket-approval.d.ts.map +1 -0
  56. package/dist/runtime/adapters/websocket-approval.js +2 -0
  57. package/dist/runtime/adapters/websocket-approval.js.map +1 -0
  58. package/dist/runtime/bun-process-runner.d.ts +2 -0
  59. package/dist/runtime/bun-process-runner.d.ts.map +1 -0
  60. package/dist/runtime/bun-process-runner.js +2 -0
  61. package/dist/runtime/bun-process-runner.js.map +1 -0
  62. package/dist/runtime.d.ts +2 -8
  63. package/dist/runtime.d.ts.map +1 -1
  64. package/dist/runtime.js +1 -7
  65. package/dist/runtime.js.map +1 -1
  66. package/dist/schema.d.ts.map +1 -1
  67. package/dist/schema.js +3 -4
  68. package/dist/schema.js.map +1 -1
  69. package/dist/tagma.d.ts +3 -4
  70. package/dist/tagma.d.ts.map +1 -1
  71. package/dist/tagma.js +2 -3
  72. package/dist/tagma.js.map +1 -1
  73. package/dist/triggers/file.d.ts.map +1 -1
  74. package/dist/triggers/file.js +74 -108
  75. package/dist/triggers/file.js.map +1 -1
  76. package/dist/triggers/manual.d.ts.map +1 -1
  77. package/dist/triggers/manual.js +1 -2
  78. package/dist/triggers/manual.js.map +1 -1
  79. package/dist/types.d.ts +1 -2
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +1 -12
  82. package/dist/types.js.map +1 -1
  83. package/dist/utils-api.d.ts +1 -1
  84. package/dist/utils-api.d.ts.map +1 -1
  85. package/dist/utils-api.js +1 -1
  86. package/dist/utils-api.js.map +1 -1
  87. package/dist/validate-raw.d.ts.map +1 -1
  88. package/dist/validate-raw.js +5 -12
  89. package/dist/validate-raw.js.map +1 -1
  90. package/package.json +20 -22
  91. package/dist/adapters/stdin-approval.d.ts +0 -6
  92. package/dist/adapters/stdin-approval.d.ts.map +0 -1
  93. package/dist/adapters/stdin-approval.js +0 -90
  94. package/dist/adapters/stdin-approval.js.map +0 -1
  95. package/dist/adapters/websocket-approval.d.ts +0 -28
  96. package/dist/adapters/websocket-approval.d.ts.map +0 -1
  97. package/dist/adapters/websocket-approval.js +0 -147
  98. package/dist/adapters/websocket-approval.js.map +0 -1
  99. package/dist/core/dataflow.d.ts +0 -23
  100. package/dist/core/dataflow.d.ts.map +0 -1
  101. package/dist/core/dataflow.js +0 -99
  102. package/dist/core/dataflow.js.map +0 -1
  103. package/dist/core/log-prune.d.ts +0 -16
  104. package/dist/core/log-prune.d.ts.map +0 -1
  105. package/dist/core/log-prune.js +0 -34
  106. package/dist/core/log-prune.js.map +0 -1
  107. package/dist/core/preflight.d.ts +0 -13
  108. package/dist/core/preflight.d.ts.map +0 -1
  109. package/dist/core/preflight.js +0 -61
  110. package/dist/core/preflight.js.map +0 -1
  111. package/dist/core/run-context.d.ts +0 -55
  112. package/dist/core/run-context.d.ts.map +0 -1
  113. package/dist/core/run-context.js +0 -158
  114. package/dist/core/run-context.js.map +0 -1
  115. package/dist/core/run-state.d.ts +0 -25
  116. package/dist/core/run-state.d.ts.map +0 -1
  117. package/dist/core/run-state.js +0 -93
  118. package/dist/core/run-state.js.map +0 -1
  119. package/dist/core/scheduler.d.ts +0 -13
  120. package/dist/core/scheduler.d.ts.map +0 -1
  121. package/dist/core/scheduler.js +0 -35
  122. package/dist/core/scheduler.js.map +0 -1
  123. package/dist/core/task-executor.d.ts +0 -13
  124. package/dist/core/task-executor.d.ts.map +0 -1
  125. package/dist/core/task-executor.js +0 -601
  126. package/dist/core/task-executor.js.map +0 -1
  127. package/dist/core/trigger-errors.d.ts +0 -9
  128. package/dist/core/trigger-errors.d.ts.map +0 -1
  129. package/dist/core/trigger-errors.js +0 -15
  130. package/dist/core/trigger-errors.js.map +0 -1
  131. package/dist/dag.d.ts +0 -45
  132. package/dist/dag.d.ts.map +0 -1
  133. package/dist/dag.js +0 -177
  134. package/dist/dag.js.map +0 -1
  135. package/dist/hooks.d.ts +0 -73
  136. package/dist/hooks.d.ts.map +0 -1
  137. package/dist/hooks.js +0 -106
  138. package/dist/hooks.js.map +0 -1
  139. package/dist/pipeline-definition.d.ts +0 -3
  140. package/dist/pipeline-definition.d.ts.map +0 -1
  141. package/dist/pipeline-definition.js +0 -4
  142. package/dist/pipeline-definition.js.map +0 -1
  143. package/dist/ports.d.ts +0 -196
  144. package/dist/ports.d.ts.map +0 -1
  145. package/dist/ports.js +0 -688
  146. package/dist/ports.js.map +0 -1
  147. package/dist/prompt-doc.d.ts +0 -70
  148. package/dist/prompt-doc.d.ts.map +0 -1
  149. package/dist/prompt-doc.js +0 -154
  150. package/dist/prompt-doc.js.map +0 -1
  151. package/dist/registry.d.ts +0 -67
  152. package/dist/registry.d.ts.map +0 -1
  153. package/dist/registry.js +0 -293
  154. package/dist/registry.js.map +0 -1
  155. package/dist/task-ref.d.ts +0 -55
  156. package/dist/task-ref.d.ts.map +0 -1
  157. package/dist/task-ref.js +0 -103
  158. package/dist/task-ref.js.map +0 -1
  159. package/dist/utils.d.ts +0 -13
  160. package/dist/utils.d.ts.map +0 -1
  161. package/dist/utils.js +0 -177
  162. package/dist/utils.js.map +0 -1
  163. package/src/adapters/stdin-approval.ts +0 -106
  164. package/src/adapters/websocket-approval.ts +0 -224
  165. package/src/approval.ts +0 -131
  166. package/src/bootstrap.ts +0 -55
  167. package/src/completions/exit-code.ts +0 -34
  168. package/src/completions/file-exists.ts +0 -66
  169. package/src/completions/output-check.test.ts +0 -50
  170. package/src/completions/output-check.ts +0 -92
  171. package/src/config-ops.test.ts +0 -70
  172. package/src/config-ops.ts +0 -328
  173. package/src/config.ts +0 -26
  174. package/src/core/dataflow.test.ts +0 -166
  175. package/src/core/dataflow.ts +0 -161
  176. package/src/core/log-prune.test.ts +0 -58
  177. package/src/core/log-prune.ts +0 -43
  178. package/src/core/preflight.test.ts +0 -49
  179. package/src/core/preflight.ts +0 -89
  180. package/src/core/run-context.test.ts +0 -256
  181. package/src/core/run-context.ts +0 -211
  182. package/src/core/run-state.test.ts +0 -98
  183. package/src/core/run-state.ts +0 -122
  184. package/src/core/scheduler.test.ts +0 -83
  185. package/src/core/scheduler.ts +0 -42
  186. package/src/core/task-executor.ts +0 -743
  187. package/src/core/trigger-errors.ts +0 -15
  188. package/src/dag.test.ts +0 -56
  189. package/src/dag.ts +0 -245
  190. package/src/drivers/opencode.ts +0 -410
  191. package/src/engine-ports-mixed.test.ts +0 -156
  192. package/src/engine-ports.test.ts +0 -166
  193. package/src/engine-task-type.test.ts +0 -56
  194. package/src/engine.ts +0 -458
  195. package/src/hooks.ts +0 -193
  196. package/src/index.ts +0 -33
  197. package/src/logger.ts +0 -182
  198. package/src/middlewares/static-context.ts +0 -49
  199. package/src/pipeline-definition.ts +0 -5
  200. package/src/pipeline-runner.test.ts +0 -91
  201. package/src/pipeline-runner.ts +0 -194
  202. package/src/plugin-registry.test.ts +0 -382
  203. package/src/plugins.ts +0 -21
  204. package/src/ports.test.ts +0 -678
  205. package/src/ports.ts +0 -925
  206. package/src/prompt-doc.test.ts +0 -174
  207. package/src/prompt-doc.ts +0 -169
  208. package/src/registry.ts +0 -353
  209. package/src/runner.test.ts +0 -142
  210. package/src/runner.ts +0 -666
  211. package/src/runtime.ts +0 -20
  212. package/src/schema-ports.test.ts +0 -172
  213. package/src/schema.test.ts +0 -213
  214. package/src/schema.ts +0 -379
  215. package/src/tagma.test.ts +0 -155
  216. package/src/tagma.ts +0 -62
  217. package/src/task-ref.test.ts +0 -401
  218. package/src/task-ref.ts +0 -121
  219. package/src/triggers/file.ts +0 -164
  220. package/src/triggers/manual.ts +0 -86
  221. package/src/types.ts +0 -18
  222. package/src/utils-api.ts +0 -8
  223. package/src/utils.test.ts +0 -28
  224. package/src/utils.ts +0 -203
  225. package/src/validate-raw-plugin-types.test.ts +0 -60
  226. package/src/validate-raw-ports.test.ts +0 -136
  227. package/src/validate-raw.ts +0 -852
  228. package/src/yaml-compiler.test.ts +0 -108
  229. package/src/yaml-compiler.ts +0 -110
  230. package/src/yaml.ts +0 -11
@@ -1,174 +0,0 @@
1
- import { describe, expect, test } from 'bun:test';
2
- import {
3
- appendContext,
4
- prependContext,
5
- promptDocumentFromString,
6
- renderInputsBlock,
7
- renderOutputSchemaBlock,
8
- serializePromptDocument,
9
- } from './prompt-doc';
10
- import type { PortDef, PromptContextBlock, PromptDocument } from './types';
11
-
12
- // ─── renderInputsBlock ────────────────────────────────────────────────
13
-
14
- describe('renderInputsBlock', () => {
15
- test('returns null when no inputs declared', () => {
16
- expect(renderInputsBlock(undefined, {})).toBeNull();
17
- expect(renderInputsBlock([], { any: 'x' })).toBeNull();
18
- });
19
-
20
- test('returns null when declared inputs have no resolved values', () => {
21
- const ports: PortDef[] = [{ name: 'city', type: 'string' }];
22
- // values missing entirely — block is noise, omit it
23
- expect(renderInputsBlock(ports, {})).toBeNull();
24
- });
25
-
26
- test('renders name: value per declared input', () => {
27
- const ports: PortDef[] = [
28
- { name: 'city', type: 'string' },
29
- { name: 'id', type: 'number' },
30
- ];
31
- const block = renderInputsBlock(ports, { city: 'Shanghai', id: 42 });
32
- expect(block).not.toBeNull();
33
- expect(block!.label).toBe('Inputs');
34
- expect(block!.content).toBe('city: "Shanghai"\nid: 42');
35
- });
36
-
37
- test('appends # description comment when provided', () => {
38
- const ports: PortDef[] = [
39
- { name: 'city', type: 'string', description: 'Target city' },
40
- ];
41
- const block = renderInputsBlock(ports, { city: 'Shanghai' })!;
42
- expect(block.content).toBe('city: "Shanghai" # Target city');
43
- });
44
-
45
- test('preserves declaration order, not input-map iteration order', () => {
46
- const ports: PortDef[] = [
47
- { name: 'b', type: 'string' },
48
- { name: 'a', type: 'string' },
49
- ];
50
- // Values object has 'a' first, 'b' second — block should still emit 'b' first.
51
- const block = renderInputsBlock(ports, { a: 'x', b: 'y' })!;
52
- expect(block.content).toBe('b: "y"\na: "x"');
53
- });
54
-
55
- test('skips ports whose values were not resolved', () => {
56
- const ports: PortDef[] = [
57
- { name: 'a', type: 'string' },
58
- { name: 'b', type: 'string' },
59
- ];
60
- const block = renderInputsBlock(ports, { a: 'x' })!;
61
- expect(block.content).toBe('a: "x"');
62
- });
63
-
64
- test('JSON-encodes non-primitive values', () => {
65
- const ports: PortDef[] = [{ name: 'payload', type: 'json' }];
66
- const block = renderInputsBlock(ports, { payload: { a: 1, b: [2, 3] } })!;
67
- expect(block.content).toBe('payload: {"a":1,"b":[2,3]}');
68
- });
69
-
70
- test('booleans render verbatim, not quoted', () => {
71
- const ports: PortDef[] = [{ name: 'flag', type: 'boolean' }];
72
- const block = renderInputsBlock(ports, { flag: true })!;
73
- expect(block.content).toBe('flag: true');
74
- });
75
- });
76
-
77
- // ─── renderOutputSchemaBlock ──────────────────────────────────────────
78
-
79
- describe('renderOutputSchemaBlock', () => {
80
- test('returns null when no outputs declared', () => {
81
- expect(renderOutputSchemaBlock(undefined)).toBeNull();
82
- expect(renderOutputSchemaBlock([])).toBeNull();
83
- });
84
-
85
- test('instructs the model to emit final-line JSON', () => {
86
- const ports: PortDef[] = [{ name: 'city', type: 'string' }];
87
- const block = renderOutputSchemaBlock(ports)!;
88
- expect(block.label).toBe('Output Format');
89
- expect(block.content).toMatch(/final line/i);
90
- });
91
-
92
- test('lists each port with its type', () => {
93
- const ports: PortDef[] = [
94
- { name: 'city', type: 'string', description: 'Target city' },
95
- { name: 'temp', type: 'number' },
96
- ];
97
- const block = renderOutputSchemaBlock(ports)!;
98
- expect(block.content).toContain('- city (string): Target city');
99
- expect(block.content).toContain('- temp (number)');
100
- });
101
-
102
- test('includes enum values in the type hint', () => {
103
- const ports: PortDef[] = [
104
- { name: 'color', type: 'enum', enum: ['red', 'green', 'blue'] },
105
- ];
106
- const block = renderOutputSchemaBlock(ports)!;
107
- expect(block.content).toContain('color (enum (one of: "red", "green", "blue"))');
108
- });
109
-
110
- test('example object uses declared defaults when present', () => {
111
- const ports: PortDef[] = [
112
- { name: 'score', type: 'number', default: 0.5 },
113
- { name: 'note', type: 'string', default: 'n/a' },
114
- ];
115
- const block = renderOutputSchemaBlock(ports)!;
116
- // The example line is `Example final line: {"score":0.5,"note":"n/a"}`.
117
- expect(block.content).toContain('"score":0.5');
118
- expect(block.content).toContain('"note":"n/a"');
119
- });
120
-
121
- test('example uses type-appropriate placeholders when no default', () => {
122
- const ports: PortDef[] = [
123
- { name: 's', type: 'string' },
124
- { name: 'n', type: 'number' },
125
- { name: 'b', type: 'boolean' },
126
- { name: 'j', type: 'json' },
127
- ];
128
- const block = renderOutputSchemaBlock(ports)!;
129
- expect(block.content).toContain('"s":"..."');
130
- expect(block.content).toContain('"n":0');
131
- expect(block.content).toContain('"b":false');
132
- expect(block.content).toContain('"j":null');
133
- });
134
-
135
- test('example uses first enum value when present', () => {
136
- const ports: PortDef[] = [
137
- { name: 'tier', type: 'enum', enum: ['low', 'high'] },
138
- ];
139
- const block = renderOutputSchemaBlock(ports)!;
140
- expect(block.content).toContain('"tier":"low"');
141
- });
142
- });
143
-
144
- // ─── prependContext / appendContext ──────────────────────────────────
145
-
146
- describe('prependContext / appendContext', () => {
147
- const block: PromptContextBlock = { label: 'X', content: 'x' };
148
-
149
- test('prependContext puts block at front without mutating input', () => {
150
- const doc: PromptDocument = { contexts: [{ label: 'Y', content: 'y' }], task: 't' };
151
- const next = prependContext(doc, block);
152
- expect(next.contexts.map((c) => c.label)).toEqual(['X', 'Y']);
153
- // Original untouched — immutability is part of the contract for
154
- // middleware safety (the engine compares doc identity to detect
155
- // changes in some paths).
156
- expect(doc.contexts).toHaveLength(1);
157
- expect(doc.contexts[0]!.label).toBe('Y');
158
- expect(next.task).toBe('t');
159
- });
160
-
161
- test('appendContext puts block at end without mutating input', () => {
162
- const doc: PromptDocument = { contexts: [{ label: 'Y', content: 'y' }], task: 't' };
163
- const next = appendContext(doc, block);
164
- expect(next.contexts.map((c) => c.label)).toEqual(['Y', 'X']);
165
- expect(doc.contexts).toHaveLength(1);
166
- });
167
-
168
- test('prepend + serialize produces [X] block above the task', () => {
169
- const doc = promptDocumentFromString('do the thing');
170
- const next = prependContext(doc, { label: 'Inputs', content: 'city: "Shanghai"' });
171
- const text = serializePromptDocument(next);
172
- expect(text).toBe('[Inputs]\ncity: "Shanghai"\n\ndo the thing');
173
- });
174
- });
package/src/prompt-doc.ts DELETED
@@ -1,169 +0,0 @@
1
- import type { PortDef, PromptContextBlock, PromptDocument } from './types';
2
-
3
- /**
4
- * Build a fresh `PromptDocument` from a raw task string.
5
- * Middlewares receive this from the engine and push context blocks onto
6
- * `contexts`. `task` is the user's original prompt and should not be
7
- * rewritten by middlewares (translation middlewares are the rare exception).
8
- */
9
- export function promptDocumentFromString(task: string): PromptDocument {
10
- return { contexts: [], task };
11
- }
12
-
13
- /**
14
- * Serialize a `PromptDocument` to the default string form consumed by
15
- * drivers that read `task.prompt` instead of `ctx.promptDoc`.
16
- *
17
- * Format:
18
- *
19
- * [<label1>]
20
- * <content1>
21
- *
22
- * [<label2>]
23
- * <content2>
24
- *
25
- * <task>
26
- *
27
- * Each context block is separated from the next (and from `task`) by a
28
- * single blank line. No implicit `[Task]` header is emitted — that framing
29
- * is the driver's responsibility (e.g. opencode's `agent_profile` wrapping).
30
- * Emitting one here would compose incorrectly with any driver that also
31
- * adds a `[Task]` header, producing a double header that some models
32
- * (observed with `opencode/big-pickle`) misread as a cut-off message.
33
- */
34
- export function serializePromptDocument(doc: PromptDocument): string {
35
- if (doc.contexts.length === 0) return doc.task;
36
- const blocks = doc.contexts.map((c) => `[${c.label}]\n${c.content}`);
37
- return `${blocks.join('\n\n')}\n\n${doc.task}`;
38
- }
39
-
40
- /**
41
- * Helper for middlewares: return a new document with the given block
42
- * appended to `contexts`, preserving immutability of `doc`.
43
- */
44
- export function appendContext(
45
- doc: PromptDocument,
46
- block: PromptContextBlock,
47
- ): PromptDocument {
48
- return { contexts: [...doc.contexts, block], task: doc.task };
49
- }
50
-
51
- /**
52
- * Helper: return a new document with the given block PREPENDED. The
53
- * engine uses this to place port-related context blocks (`[Inputs]`,
54
- * `[Output Format]`) at the top of the document so middlewares that
55
- * assemble retrieval context against the task's inputs see them.
56
- */
57
- export function prependContext(
58
- doc: PromptDocument,
59
- block: PromptContextBlock,
60
- ): PromptDocument {
61
- return { contexts: [block, ...doc.contexts], task: doc.task };
62
- }
63
-
64
- /**
65
- * Build an `[Inputs]` context block from a map of resolved port inputs.
66
- * Each input is rendered on its own line as `name: <value>` with an
67
- * optional trailing `# <description>` comment so the model has both the
68
- * value and the reason it matters.
69
- *
70
- * The block is *only* useful for AI tasks; command tasks consume inputs
71
- * through `{{inputs.X}}` substitution in their command line and do not
72
- * need this context.
73
- *
74
- * Returns null when there are no inputs to render — callers can forward
75
- * that nullish value to `prependContext` via an `if (block)` check so
76
- * empty-input tasks don't grow a noise block in their prompt.
77
- */
78
- export function renderInputsBlock(
79
- inputsDecl: readonly PortDef[] | undefined,
80
- values: Readonly<Record<string, unknown>>,
81
- ): PromptContextBlock | null {
82
- if (!inputsDecl || inputsDecl.length === 0) return null;
83
- const lines: string[] = [];
84
- for (const port of inputsDecl) {
85
- if (!(port.name in values)) continue;
86
- const raw = values[port.name];
87
- const rendered = renderInputValue(raw);
88
- const descr = port.description?.trim();
89
- lines.push(descr ? `${port.name}: ${rendered} # ${descr}` : `${port.name}: ${rendered}`);
90
- }
91
- if (lines.length === 0) return null;
92
- return { label: 'Inputs', content: lines.join('\n') };
93
- }
94
-
95
- function renderInputValue(value: unknown): string {
96
- if (value === null || value === undefined) return '';
97
- if (typeof value === 'string') return JSON.stringify(value);
98
- if (typeof value === 'number' || typeof value === 'boolean') return String(value);
99
- try {
100
- return JSON.stringify(value);
101
- } catch {
102
- return String(value);
103
- }
104
- }
105
-
106
- /**
107
- * Build an `[Output Format]` context block from a task's declared output
108
- * ports. The block instructs the model to emit a final-line JSON object
109
- * matching the declared schema so `extractTaskOutputs` can pick it up
110
- * without fragile heuristics. Returns null when the task declares no
111
- * outputs.
112
- *
113
- * The instruction is deliberately short and explicit — a terse "emit
114
- * this object as JSON on the final line" beats a long schema dump
115
- * because shorter prompts compose better with downstream middlewares.
116
- */
117
- export function renderOutputSchemaBlock(
118
- outputsDecl: readonly PortDef[] | undefined,
119
- ): PromptContextBlock | null {
120
- if (!outputsDecl || outputsDecl.length === 0) return null;
121
- const lines: string[] = [];
122
- lines.push(
123
- 'After your response, emit a single JSON object on the FINAL line with these keys:',
124
- );
125
- for (const port of outputsDecl) {
126
- const descr = port.description?.trim();
127
- const enumHint =
128
- port.type === 'enum' && port.enum?.length
129
- ? ` (one of: ${port.enum.map((v) => JSON.stringify(v)).join(', ')})`
130
- : '';
131
- lines.push(
132
- descr
133
- ? ` - ${port.name} (${port.type}${enumHint}): ${descr}`
134
- : ` - ${port.name} (${port.type}${enumHint})`,
135
- );
136
- }
137
- const example = buildExampleObject(outputsDecl);
138
- lines.push('');
139
- lines.push(`Example final line: ${JSON.stringify(example)}`);
140
- return { label: 'Output Format', content: lines.join('\n') };
141
- }
142
-
143
- function buildExampleObject(outputsDecl: readonly PortDef[]): Record<string, unknown> {
144
- const example: Record<string, unknown> = {};
145
- for (const port of outputsDecl) {
146
- if (port.default !== undefined) {
147
- example[port.name] = port.default;
148
- continue;
149
- }
150
- switch (port.type) {
151
- case 'string':
152
- example[port.name] = '...';
153
- break;
154
- case 'number':
155
- example[port.name] = 0;
156
- break;
157
- case 'boolean':
158
- example[port.name] = false;
159
- break;
160
- case 'enum':
161
- example[port.name] = port.enum?.[0] ?? '...';
162
- break;
163
- case 'json':
164
- default:
165
- example[port.name] = null;
166
- }
167
- }
168
- return example;
169
- }
package/src/registry.ts DELETED
@@ -1,353 +0,0 @@
1
- import { createRequire } from 'node:module';
2
- import { pathToFileURL } from 'node:url';
3
- import type {
4
- CapabilityHandler,
5
- PluginCategory,
6
- DriverPlugin,
7
- TriggerPlugin,
8
- CompletionPlugin,
9
- MiddlewarePlugin,
10
- PluginManifest,
11
- TagmaPlugin,
12
- } from './types';
13
-
14
- type PluginType = CapabilityHandler;
15
-
16
- const CAPABILITY_CATEGORIES = [
17
- 'drivers',
18
- 'triggers',
19
- 'completions',
20
- 'middlewares',
21
- ] as const satisfies readonly PluginCategory[];
22
-
23
- const VALID_CATEGORIES: ReadonlySet<PluginCategory> = new Set(CAPABILITY_CATEGORIES);
24
- const PLUGIN_TYPE_RE = /^[A-Za-z_][A-Za-z0-9_-]*$/;
25
-
26
- export interface RegisteredCapability {
27
- readonly category: PluginCategory;
28
- readonly type: string;
29
- readonly result: RegisterResult;
30
- }
31
-
32
- function singularCategory(category: PluginCategory): string {
33
- switch (category) {
34
- case 'drivers':
35
- return 'driver';
36
- case 'triggers':
37
- return 'trigger';
38
- case 'completions':
39
- return 'completion';
40
- case 'middlewares':
41
- return 'middleware';
42
- }
43
- }
44
-
45
- /**
46
- * Minimal contract enforcement so a malformed plugin fails fast at
47
- * registration time rather than crashing the engine mid-run.
48
- *
49
- * For drivers we materialize `capabilities` and assert each field is a
50
- * boolean —otherwise a plugin author can write
51
- * get capabilities() { throw new Error('boom') }
52
- * and pass the basic typeof check, then crash preflight when the engine
53
- * touches `driver.capabilities.sessionResume`. (R8)
54
- */
55
- function validateContract(category: PluginCategory, handler: unknown): void {
56
- if (!handler || typeof handler !== 'object') {
57
- throw new Error(`Plugin handler for category "${category}" must be an object`);
58
- }
59
- const h = handler as Record<string, unknown>;
60
- if (typeof h.name !== 'string' || h.name.length === 0) {
61
- throw new Error(`Plugin handler for category "${category}" must declare a non-empty "name"`);
62
- }
63
- switch (category) {
64
- case 'drivers': {
65
- if (typeof h.buildCommand !== 'function') {
66
- throw new Error(`drivers plugin "${h.name}" must export buildCommand()`);
67
- }
68
- // Materialize capabilities —this triggers any throwing getter NOW
69
- // instead of during preflight.
70
- let caps: unknown;
71
- try {
72
- caps = h.capabilities;
73
- } catch (err) {
74
- throw new Error(
75
- `drivers plugin "${h.name}" capabilities accessor threw: ` +
76
- (err instanceof Error ? err.message : String(err)),
77
- );
78
- }
79
- if (!caps || typeof caps !== 'object') {
80
- throw new Error(`drivers plugin "${h.name}" must declare capabilities object`);
81
- }
82
- const c = caps as Record<string, unknown>;
83
- for (const field of ['sessionResume', 'systemPrompt', 'outputFormat'] as const) {
84
- if (typeof c[field] !== 'boolean') {
85
- throw new Error(
86
- `drivers plugin "${h.name}".capabilities.${field} must be a boolean (got ${typeof c[field]})`,
87
- );
88
- }
89
- }
90
- // Optional methods, but if present must be functions.
91
- for (const opt of ['parseResult', 'resolveModel', 'resolveTools'] as const) {
92
- if (h[opt] !== undefined && typeof h[opt] !== 'function') {
93
- throw new Error(`drivers plugin "${h.name}".${opt} must be a function or undefined`);
94
- }
95
- }
96
- break;
97
- }
98
- case 'triggers':
99
- if (typeof h.watch !== 'function') {
100
- throw new Error(`triggers plugin "${h.name}" must export watch()`);
101
- }
102
- break;
103
- case 'completions':
104
- if (typeof h.check !== 'function') {
105
- throw new Error(`completions plugin "${h.name}" must export check()`);
106
- }
107
- break;
108
- case 'middlewares':
109
- if (typeof h.enhanceDoc !== 'function') {
110
- throw new Error(
111
- `middlewares plugin "${h.name}" must export enhanceDoc()`,
112
- );
113
- }
114
- break;
115
- }
116
- }
117
-
118
- export type RegisterResult = 'registered' | 'replaced' | 'unchanged';
119
-
120
- // Plugin name must be a scoped npm package or a tagma-prefixed package.
121
- // Reject absolute/relative paths and suspicious patterns to prevent
122
- // arbitrary code execution via crafted YAML configs.
123
- export const PLUGIN_NAME_RE = /^(@[a-z0-9-]+\/[a-z0-9._-]+|tagma-plugin-[a-z0-9._-]+)$/;
124
-
125
- export function isValidPluginName(name: unknown): name is string {
126
- return typeof name === 'string' && PLUGIN_NAME_RE.test(name);
127
- }
128
-
129
- /**
130
- * Parse and validate the `tagmaPlugin` field of a `package.json` blob.
131
- *
132
- * Returns the strongly-typed manifest if the field is present and
133
- * well-formed (`category` is one of the four known categories and `type`
134
- * is a non-empty string). Returns `null` if the field is absent —that
135
- * is the host's signal that the package is a library, not a plugin.
136
- *
137
- * Throws if the field is present but malformed: that's a packaging bug
138
- * the plugin author should hear about loudly, not a silent skip.
139
- *
140
- * Hosts use this during auto-discovery to decide whether to load a
141
- * package as a plugin without having to dynamically `import()` it.
142
- */
143
- export function readPluginManifest(pkgJson: unknown): PluginManifest | null {
144
- if (!pkgJson || typeof pkgJson !== 'object') return null;
145
- const raw = (pkgJson as Record<string, unknown>).tagmaPlugin;
146
- if (raw === undefined) return null;
147
- if (!raw || typeof raw !== 'object') {
148
- throw new Error('tagmaPlugin field must be an object with { category, type }');
149
- }
150
- const m = raw as Record<string, unknown>;
151
- const category = m.category;
152
- const type = m.type;
153
- if (typeof category !== 'string' || !VALID_CATEGORIES.has(category as PluginCategory)) {
154
- throw new Error(
155
- `tagmaPlugin.category must be one of ${[...VALID_CATEGORIES].join(', ')}, got ${JSON.stringify(category)}`,
156
- );
157
- }
158
- if (typeof type !== 'string' || type.length === 0) {
159
- throw new Error(`tagmaPlugin.type must be a non-empty string, got ${JSON.stringify(type)}`);
160
- }
161
- if (!PLUGIN_TYPE_RE.test(type)) {
162
- throw new Error(
163
- `tagmaPlugin.type must match ${PLUGIN_TYPE_RE} (letters, digits, underscores, hyphens; no paths or dots), got ${JSON.stringify(type)}`,
164
- );
165
- }
166
- return { category: category as PluginCategory, type };
167
- }
168
-
169
- function isRecord(value: unknown): value is Record<string, unknown> {
170
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
171
- }
172
-
173
- function isTagmaPlugin(value: unknown): value is TagmaPlugin {
174
- if (!isRecord(value)) return false;
175
- if (typeof value.name !== 'string' || value.name.length === 0) return false;
176
- if (value.capabilities !== undefined && !isRecord(value.capabilities)) return false;
177
- if (value.setup !== undefined && typeof value.setup !== 'function') return false;
178
- return true;
179
- }
180
-
181
- function hasSupportedCapabilityMap(plugin: TagmaPlugin): boolean {
182
- if (!plugin.capabilities) return false;
183
- const capabilities = plugin.capabilities as Record<string, unknown>;
184
- return CAPABILITY_CATEGORIES.some((category) => capabilities[category] !== undefined);
185
- }
186
-
187
- function moduleDefaultPlugin(name: string, mod: unknown): TagmaPlugin {
188
- if (!isRecord(mod) || !isTagmaPlugin(mod.default) || !hasSupportedCapabilityMap(mod.default)) {
189
- throw new Error(
190
- `Plugin "${name}" must default-export a TagmaPlugin with capabilities maps`,
191
- );
192
- }
193
- return mod.default;
194
- }
195
-
196
- /**
197
- * Instance-scoped plugin registry. Each workspace in a multi-tenant sidecar
198
- * owns its own PluginRegistry, so installing/uninstalling a driver in one
199
- * workspace cannot clobber another.
200
- */
201
- export class PluginRegistry {
202
- private readonly registries = {
203
- drivers: new Map<string, DriverPlugin>(),
204
- triggers: new Map<string, TriggerPlugin>(),
205
- completions: new Map<string, CompletionPlugin>(),
206
- middlewares: new Map<string, MiddlewarePlugin>(),
207
- };
208
-
209
- /**
210
- * Register a plugin under (category, type). Returns:
211
- * - 'registered' on first registration
212
- * - 'replaced' when an existing entry was overwritten with a different handler
213
- * - 'unchanged' when the same handler instance was already present
214
- *
215
- * Throws if `category` is unknown, `type` is empty, or `handler` violates
216
- * the minimum interface contract for the category.
217
- */
218
- registerPlugin<T extends PluginType>(
219
- category: PluginCategory,
220
- type: string,
221
- handler: T,
222
- ): RegisterResult {
223
- if (!VALID_CATEGORIES.has(category)) {
224
- throw new Error(`Unknown plugin category "${category}"`);
225
- }
226
- if (typeof type !== 'string' || type.length === 0) {
227
- throw new Error(`Plugin type must be a non-empty string (category="${category}")`);
228
- }
229
- if (!PLUGIN_TYPE_RE.test(type)) {
230
- throw new Error(
231
- `Plugin type "${type}" must match ${PLUGIN_TYPE_RE} (letters, digits, underscores, hyphens; no paths or dots)`,
232
- );
233
- }
234
- validateContract(category, handler);
235
- const registry = this.registries[category] as Map<string, T>;
236
- const existing = registry.get(type);
237
- if (existing === handler) return 'unchanged';
238
- const wasReplaced = existing !== undefined;
239
- registry.set(type, handler);
240
- if (wasReplaced) {
241
- // D18: surface silent shadowing. Hot-reload flows legitimately replace
242
- // handlers; installing two different plugin packages that both claim
243
- // the same (category, type) does not —the second wins and breaks the
244
- // first's consumers with no audit trail. A console.warn is cheap,
245
- // respects existing callers that rely on 'replaced', and gives ops a
246
- // grep-able signal when registrations collide unexpectedly.
247
- console.warn(
248
- `[tagma-sdk] registerPlugin: replaced existing ${category}/${type} - ` +
249
- `check for duplicate plugin packages claiming the same type.`,
250
- );
251
- }
252
- return wasReplaced ? 'replaced' : 'registered';
253
- }
254
-
255
- registerTagmaPlugin(plugin: TagmaPlugin): RegisteredCapability[] {
256
- if (!isTagmaPlugin(plugin)) {
257
- throw new Error('TagmaPlugin must be an object with a non-empty "name"');
258
- }
259
- if (!plugin.capabilities) {
260
- throw new Error(`TagmaPlugin "${plugin.name}" must declare capabilities`);
261
- }
262
-
263
- const registered: RegisteredCapability[] = [];
264
- const capabilities = plugin.capabilities as Record<string, unknown>;
265
- for (const category of CAPABILITY_CATEGORIES) {
266
- const handlers = capabilities[category];
267
- if (handlers === undefined) continue;
268
- if (!isRecord(handlers)) {
269
- throw new Error(
270
- `TagmaPlugin "${plugin.name}" capabilities.${category} must be an object map`,
271
- );
272
- }
273
- for (const [type, handler] of Object.entries(handlers)) {
274
- const result = this.registerPlugin(category, type, handler as PluginType);
275
- registered.push({ category, type, result });
276
- }
277
- }
278
-
279
- if (registered.length === 0) {
280
- throw new Error(
281
- `TagmaPlugin "${plugin.name}" must declare at least one supported capability`,
282
- );
283
- }
284
- return registered;
285
- }
286
-
287
- /**
288
- * Remove a plugin from the in-process registry. Returns true if a plugin
289
- * was actually removed. Note: ESM module caching is not affected, so
290
- * re-importing the same file after unregister will yield the cached module — * callers wanting a fresh load must restart the host process.
291
- */
292
- unregisterPlugin(category: PluginCategory, type: string): boolean {
293
- if (!VALID_CATEGORIES.has(category)) return false;
294
- return this.registries[category].delete(type);
295
- }
296
-
297
- getHandler<T extends PluginType>(category: PluginCategory, type: string): T {
298
- const handler = this.registries[category].get(type);
299
- if (!handler) {
300
- throw new Error(
301
- `${category} type "${type}" not registered.\n` +
302
- `Install the plugin: bun add @tagma/${singularCategory(category)}-${type}`,
303
- );
304
- }
305
- return handler as T;
306
- }
307
-
308
- hasHandler(category: PluginCategory, type: string): boolean {
309
- return this.registries[category].has(type);
310
- }
311
-
312
- listRegistered(category: PluginCategory): string[] {
313
- return [...this.registries[category].keys()];
314
- }
315
-
316
- /**
317
- * Load and register a list of plugin packages into this registry.
318
- *
319
- * @param pluginNames - Validated npm package names to load.
320
- * @param resolveFrom - Optional absolute path to resolve plugins from (e.g.
321
- * the workspace's working directory). When omitted, the default ESM
322
- * resolution uses the SDK's own `node_modules`, which will fail for
323
- * plugins installed only in the user's workspace. CLI callers should
324
- * pass `process.cwd()` or the workspace root so that workspace-local
325
- * plugins resolve correctly.
326
- */
327
- async loadPlugins(
328
- pluginNames: readonly string[],
329
- resolveFrom?: string,
330
- ): Promise<void> {
331
- for (const name of pluginNames) {
332
- if (!isValidPluginName(name)) {
333
- throw new Error(
334
- `Plugin "${name}" rejected: plugin names must be scoped npm packages ` +
335
- `(e.g. @tagma/trigger-xyz) or tagma-plugin-* packages. ` +
336
- `Relative/absolute paths are not allowed.`,
337
- );
338
- }
339
- let moduleUrl: string = name;
340
- if (resolveFrom) {
341
- // Resolve the package entry point relative to the caller's directory
342
- // so plugins installed in the workspace's node_modules are found
343
- // even when the SDK itself lives elsewhere (e.g. a global install
344
- // or a monorepo sibling package).
345
- const req = createRequire(resolveFrom.endsWith('/') ? resolveFrom : resolveFrom + '/');
346
- const resolved = req.resolve(name);
347
- moduleUrl = pathToFileURL(resolved).href;
348
- }
349
- const mod = await import(moduleUrl);
350
- this.registerTagmaPlugin(moduleDefaultPlugin(name, mod));
351
- }
352
- }
353
- }