@kindgi/cli 0.2.0 → 0.3.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/README.md +12 -5
- package/dist/cli.js +4 -3
- package/dist/cli.js.map +1 -1
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +75 -19
- package/dist/commands/agents.js.map +1 -1
- package/dist/commands/approval-wait.d.ts +90 -0
- package/dist/commands/approval-wait.d.ts.map +1 -0
- package/dist/commands/approval-wait.js +154 -0
- package/dist/commands/approval-wait.js.map +1 -0
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +131 -14
- package/dist/commands/artifacts.js.map +1 -1
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +48 -5
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/deploy.js +7 -3
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/deployments.d.ts.map +1 -1
- package/dist/commands/deployments.js +36 -13
- package/dist/commands/deployments.js.map +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +47 -14
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/eval-runs.d.ts.map +1 -1
- package/dist/commands/eval-runs.js +21 -9
- package/dist/commands/eval-runs.js.map +1 -1
- package/dist/commands/helpers.d.ts +31 -1
- package/dist/commands/helpers.d.ts.map +1 -1
- package/dist/commands/helpers.js +45 -2
- package/dist/commands/helpers.js.map +1 -1
- package/dist/commands/init.js +5 -4
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mail-triggers.d.ts.map +1 -1
- package/dist/commands/mail-triggers.js +27 -3
- package/dist/commands/mail-triggers.js.map +1 -1
- package/dist/commands/promote.d.ts.map +1 -1
- package/dist/commands/promote.js +133 -11
- package/dist/commands/promote.js.map +1 -1
- package/dist/commands/provenance.d.ts.map +1 -1
- package/dist/commands/provenance.js +13 -5
- package/dist/commands/provenance.js.map +1 -1
- package/dist/commands/providers.d.ts.map +1 -1
- package/dist/commands/providers.js +47 -4
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/schedules.d.ts.map +1 -1
- package/dist/commands/schedules.js +25 -9
- package/dist/commands/schedules.js.map +1 -1
- package/dist/commands/skills.d.ts +16 -0
- package/dist/commands/skills.d.ts.map +1 -1
- package/dist/commands/skills.js +122 -36
- package/dist/commands/skills.js.map +1 -1
- package/dist/commands/webhooks.d.ts.map +1 -1
- package/dist/commands/webhooks.js +21 -5
- package/dist/commands/webhooks.js.map +1 -1
- package/dist/config.d.ts +8 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +7 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +1 -0
- package/dist/context.js.map +1 -1
- package/dist/dev/runtime-image.d.ts +1 -1
- package/dist/dev/runtime-image.js +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -1
- package/dist/errors.js.map +1 -1
- package/dist/exports/plan.d.ts.map +1 -1
- package/dist/exports/plan.js +50 -0
- package/dist/exports/plan.js.map +1 -1
- package/dist/init/augment-scaffolder.d.ts +1 -1
- package/dist/init/augment-scaffolder.js +9 -8
- package/dist/init/augment-scaffolder.js.map +1 -1
- package/dist/init/java-augment.d.ts.map +1 -1
- package/dist/init/java-augment.js +7 -6
- package/dist/init/java-augment.js.map +1 -1
- package/dist/init/python-augment.d.ts.map +1 -1
- package/dist/init/python-augment.js +7 -6
- package/dist/init/python-augment.js.map +1 -1
- package/dist/init/scala-augment.js +7 -6
- package/dist/init/scala-augment.js.map +1 -1
- package/dist/main.d.ts +3 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/providers/presets/anthropic.json +15 -3
- package/dist/providers/presets/azure-openai.json +10 -2
- package/dist/providers/presets/bedrock.json +6 -2
- package/dist/providers/presets/gemini-api.json +6 -2
- package/dist/providers/presets/openai.json +15 -3
- package/dist/sdk-skills/kindgi-authoring-agents/SKILL.md +21 -9
- package/dist/sdk-skills/kindgi-authoring-flows/SKILL.md +5 -3
- package/dist/sdk-skills/kindgi-authoring-guardrails/SKILL.md +202 -68
- package/dist/sdk-skills/kindgi-authoring-mcp-servers/SKILL.md +12 -188
- package/dist/sdk-skills/kindgi-authoring-mcp-servers/common-mistakes.md +44 -0
- package/dist/sdk-skills/kindgi-authoring-mcp-servers/how-it-works.md +41 -0
- package/dist/sdk-skills/kindgi-authoring-mcp-servers/other-clients.md +19 -0
- package/dist/sdk-skills/kindgi-authoring-mcp-servers/postgres.md +45 -0
- package/dist/sdk-skills/kindgi-authoring-mcp-servers/several-servers.md +32 -0
- package/dist/sdk-skills/kindgi-authoring-providers/SKILL.md +49 -906
- package/dist/sdk-skills/kindgi-authoring-providers/bedrock.md +64 -0
- package/dist/sdk-skills/kindgi-authoring-providers/common-mistakes.md +125 -0
- package/dist/sdk-skills/kindgi-authoring-providers/keys-and-registration.md +58 -0
- package/dist/sdk-skills/kindgi-authoring-providers/kindgi-cloud.md +36 -0
- package/dist/sdk-skills/kindgi-authoring-providers/path-a-anthropic.md +216 -0
- package/dist/sdk-skills/kindgi-authoring-providers/path-b-openai-compat.md +127 -0
- package/dist/sdk-skills/kindgi-authoring-providers/path-c-onnx.md +97 -0
- package/dist/sdk-skills/kindgi-authoring-providers/path-d-vertex.md +95 -0
- package/dist/sdk-skills/kindgi-authoring-providers/router.md +113 -0
- package/dist/sdk-skills/kindgi-authoring-tools/SKILL.md +12 -387
- package/dist/sdk-skills/kindgi-authoring-tools/common-mistakes.md +135 -0
- package/dist/sdk-skills/kindgi-authoring-tools/declarative-http.md +34 -0
- package/dist/sdk-skills/kindgi-authoring-tools/imperative-handler.md +65 -0
- package/dist/sdk-skills/kindgi-authoring-tools/iterating-and-testing.md +45 -0
- package/dist/sdk-skills/kindgi-authoring-tools/secrets-and-connections.md +79 -0
- package/dist/sdk-skills/kindgi-authoring-tools/tool-flags.md +45 -0
- package/dist/sdk-skills/kindgi-fix-corrections/SKILL.md +13 -178
- package/dist/sdk-skills/kindgi-fix-corrections/make-the-change.md +25 -0
- package/dist/sdk-skills/kindgi-fix-corrections/publish-and-promote.md +76 -0
- package/dist/sdk-skills/kindgi-fix-corrections/pull-corrections.md +48 -0
- package/dist/sdk-skills/kindgi-fix-corrections/worked-example.md +27 -0
- package/dist/sdk-skills/kindgi-framework-feedback/SKILL.md +10 -180
- package/dist/sdk-skills/kindgi-framework-feedback/common-mistakes.md +19 -0
- package/dist/sdk-skills/kindgi-framework-feedback/editing-feedback-file.md +33 -0
- package/dist/sdk-skills/kindgi-framework-feedback/fields-and-entry-shape.md +53 -0
- package/dist/sdk-skills/kindgi-framework-feedback/hearing-back.md +24 -0
- package/dist/sdk-skills/kindgi-framework-feedback/sending.md +50 -0
- package/dist/sdk-skills/kindgi-getting-started/SKILL.md +24 -21
- package/dist/sdk-skills/kindgi-java-authoring-agents/SKILL.md +12 -4
- package/dist/sdk-skills/kindgi-java-authoring-flows/SKILL.md +6 -4
- package/dist/sdk-skills/kindgi-java-authoring-guardrails/SKILL.md +33 -23
- package/dist/sdk-skills/kindgi-java-authoring-tools/SKILL.md +19 -12
- package/dist/sdk-skills/kindgi-java-getting-started/SKILL.md +16 -16
- package/dist/sdk-skills/kindgi-python-authoring-agents/SKILL.md +18 -6
- package/dist/sdk-skills/kindgi-python-authoring-flows/SKILL.md +6 -4
- package/dist/sdk-skills/kindgi-python-authoring-guardrails/SKILL.md +39 -30
- package/dist/sdk-skills/kindgi-python-authoring-tools/SKILL.md +19 -9
- package/dist/sdk-skills/kindgi-python-getting-started/SKILL.md +24 -21
- package/dist/sdk-skills/kindgi-scala-authoring-agents/SKILL.md +12 -4
- package/dist/sdk-skills/kindgi-scala-authoring-flows/SKILL.md +6 -4
- package/dist/sdk-skills/kindgi-scala-authoring-guardrails/SKILL.md +34 -23
- package/dist/sdk-skills/kindgi-scala-authoring-tools/SKILL.md +15 -9
- package/dist/sdk-skills/kindgi-scala-getting-started/SKILL.md +16 -16
- package/dist/sdk-skills/kindgi-using/SKILL.md +11 -11
- package/dist/templates/sample/README.md.tmpl +8 -7
- package/dist/templates/sample/agents/echo-agent/index.ts.tmpl +1 -1
- package/dist/templates/sample/kindgi.config.ts.tmpl +1 -1
- package/dist/templates/sample/package.json.tmpl +1 -1
- package/dist/templates/woocommerce/README.md.tmpl +9 -3
- package/package.json +13 -13
|
@@ -12,8 +12,8 @@ description: >
|
|
|
12
12
|
authoring agents is covered by kindgi-authoring-agents.
|
|
13
13
|
type: core
|
|
14
14
|
library: "@kindgi/sdk"
|
|
15
|
-
version: "0.
|
|
16
|
-
sdk_version: "0.
|
|
15
|
+
version: "0.5.1"
|
|
16
|
+
sdk_version: "0.3.0"
|
|
17
17
|
pack_languages: [node]
|
|
18
18
|
sources:
|
|
19
19
|
- packages/tools/src/types.ts
|
|
@@ -60,227 +60,16 @@ logic lives:
|
|
|
60
60
|
`handler` and `spec` are MUTUALLY EXCLUSIVE. Setting both (or neither)
|
|
61
61
|
makes `defineTool` return an `invalid-tool-definition` error.
|
|
62
62
|
|
|
63
|
-
##
|
|
63
|
+
## Your case: read this file
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
jurisdiction: z.enum(['US', 'UK', 'EU']),
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const Output = z.object({
|
|
77
|
-
found: z.boolean(),
|
|
78
|
-
canonicalCite: z.string().optional(),
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const defined = defineTool({
|
|
82
|
-
id: 'acme.verify-citation' as ToolId,
|
|
83
|
-
description:
|
|
84
|
-
'Verify a legal citation against the jurisdictional citator. Returns whether the citation resolves and the canonical form.',
|
|
85
|
-
version: '0.1.0',
|
|
86
|
-
input: Input,
|
|
87
|
-
output: Output,
|
|
88
|
-
effects: [],
|
|
89
|
-
mutating: false,
|
|
90
|
-
handler: async (input, ctx) => {
|
|
91
|
-
// ctx.tenantId, ctx.abortSignal, ctx.secrets (what needsSpec declares) available;
|
|
92
|
-
// in a run, ctx.projectId and ctx.orgId (set from the run, never from the input)
|
|
93
|
-
// Return type MUST match Output schema (validated at invoke time)
|
|
94
|
-
return { found: true, canonicalCite: '...' };
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
if (defined.kind === 'err') {
|
|
99
|
-
throw new Error(`acme.verify-citation failed to compile: ${defined.error.message}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export default defined.value;
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
A handler must return a Promise; one with nothing to `await` can return `Promise.resolve({ … })` instead of being `async`, which keeps lint rules like `require-await` quiet.
|
|
106
|
-
|
|
107
|
-
### What the handler receives
|
|
108
|
-
|
|
109
|
-
The handler gets the **parsed** input, typed `z.infer` of `input` (Zod's output type):
|
|
110
|
-
|
|
111
|
-
- **Defaults.** A `.default()` field is optional to the caller, the model included. The tool's advertised schema doesn't list it as required, and the handler always gets a value.
|
|
112
|
-
- **Transforms and refinements.** `.transform()` results and `.refine()` checks apply before the handler runs. A failed refinement comes back as `input-validation-failed`.
|
|
113
|
-
- **Extra keys.** A plain `z.object` accepts them and strips them. Use `z.strictObject` to reject them.
|
|
114
|
-
- **JSON-Schema-authored tools** get each property's `default` filled in the same way.
|
|
115
|
-
- **Unions of scalars** (`z.union([z.string(), z.number()])`, `type: ['string', 'number']` on the wire) compile and validate. A schema Kindgi's strict schema check still refuses (an open tuple, an unknown keyword) says so at `defineTool`; for a field that may hold any JSON value, `z.json()` compiles.
|
|
116
|
-
|
|
117
|
-
The output side is the reverse: the advertised output schema requires every field, defaulted ones included. Return them all.
|
|
118
|
-
|
|
119
|
-
### Logging from the handler
|
|
120
|
-
|
|
121
|
-
`ctx.log` is a logger bound to the call: its records carry the run's ids, the tool's id and the trace id. It has `info`, `warn`, `error`, `debug` and `trace`, each `(message, fields?)`. The pack service sets it; a context your own code builds (a test's) may not, so write `ctx.log?.`:
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
ctx.log?.info('refund issued', { orderId, amountCents });
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Put values in `fields`, never in the message. Log ids, amounts and outcomes, never what a person typed (a refund's reason, a message, an address): the log is read by whoever operates the runtime, not only by the person the data is about. Docs: https://docs.kindgi.com/v0.2/guides/tools/write-a-tool/#log-from-a-tool
|
|
128
|
-
|
|
129
|
-
### Configuration and secrets
|
|
130
|
-
|
|
131
|
-
A secret that belongs to the tenant — an API key a customer gives you — is declared, and read from `ctx.secrets`:
|
|
132
|
-
|
|
133
|
-
```ts
|
|
134
|
-
const defined = defineTool({
|
|
135
|
-
// …id, description, version, input, output, effects…
|
|
136
|
-
needsSpec: { secrets: { CITATOR_KEY: { type: 'string', minLength: 20 } } },
|
|
137
|
-
handler: async (input, ctx) => {
|
|
138
|
-
const key = ctx.secrets?.CITATOR_KEY;
|
|
139
|
-
// …
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
The runtime resolves every declared secret on every call, for the call's tenant, in its env (`KINDGI_ENV`; in `kindgi dev`, `local`: the pack's `.env` and `.env.local`, then Kindgi's own `.kindgi/secrets.env`, where `kindgi secrets set` writes). It checks each value against its schema, and fails the call, naming the secret, when one is missing or doesn't match. A declared secret is required, so in a runtime call `ctx.secrets` holds it; it's optional in the type because a unit test builds its own context and passes `secrets: { CITATOR_KEY: '…' }`. **An optional secret** has a schema that names `null` (`{ type: ['string', 'null'] }`; an unconstrained `{}` stays required): one never stored, or empty, is left out of `ctx.secrets`, and the call goes on, its log line naming it. One revoked, or gone at its provider though mapped, still fails the call. Optional secrets need runtime 0.1.6 or later; an older runtime requires them.
|
|
145
|
-
|
|
146
|
-
A value that differs per tenant, org or project but isn't secret (a base URL, a region, an account id) is an **env value**: declared in `needsSpec.env`, read from `ctx.env`:
|
|
147
|
-
|
|
148
|
-
```ts
|
|
149
|
-
const defined = defineTool({
|
|
150
|
-
// …id, description, version, input, output, effects…
|
|
151
|
-
needsSpec: {
|
|
152
|
-
env: {
|
|
153
|
-
ORDERS_BASE_URL: { type: 'string', pattern: '^https://' },
|
|
154
|
-
ORDERS_REGION: { type: 'string', enum: ['eu', 'us'], default: 'eu' },
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
handler: async ({ orderId }, ctx) => ({
|
|
158
|
-
url: `${ctx.env?.ORDERS_BASE_URL}/${ctx.env?.ORDERS_REGION}/orders/${orderId}`,
|
|
159
|
-
}),
|
|
160
|
-
});
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
- **Which value a call gets:** its project's, else its org's, else the tenant's, in the runtime's env; a schema `default` makes a name optional. The values a call used are recorded with it, so a retry or a resume sees the same ones.
|
|
164
|
-
- **Setting them:** `kindgi env set ORDERS_REGION us --scope=project:<project-id> --env=local` (or `--scope=tenant`, for every project). Changing a value that's already set takes `--force`.
|
|
165
|
-
- **A declared value nobody set** stops the call before the tool runs. For a tool `acme-orders.needs-account` that declares `ACME_ACCOUNT_ID`, the message reads:
|
|
166
|
-
```text
|
|
167
|
-
precondition-failed: Tool "acme-orders.needs-account" was not run: env-value-missing: tool "acme-orders.needs-account" needs env value "ACME_ACCOUNT_ID" in env "local", and none is set for project e889c1f5-eae7-45dc-8669-5bd029a5d85c, its org, or the tenant. Set it: kindgi env set ACME_ACCOUNT_ID <value> --scope=project:e889c1f5-eae7-45dc-8669-5bd029a5d85c --env=local (or --scope=tenant, for every project)
|
|
168
|
-
```
|
|
169
|
-
- **Not secret:** env values are recorded with each run that uses them and shown in its journal. A credential is a secret (`needsSpec.secrets`), never an env value.
|
|
170
|
-
- **Schemas compile strictly:** each `needsSpec` schema must compile as the runtime compiles it (an unknown keyword is refused), and an env `default` is a string; otherwise `defineTool` returns `invalid-tool-definition`, and a deploy fails with `deployment-validation-failed`.
|
|
171
|
-
- **In a unit test:** call it with `invokeToolForTest(tool, input, { env: { … } })` (`@kindgi/sdk/define`). It decides the values as a run does: a name you don't set takes its schema's `default`, each value is checked, and the handler sees the declared names only. A missing or invalid one is `precondition-failed` (`env-value-missing`, `env-value-invalid`). Plain `invokeTool` passes `ctx.env` as given (the runtime decides a pack tool's env itself).
|
|
172
|
-
|
|
173
|
-
Everything else comes from the process environment: `process.env.CITATOR_URL`. The pack service runs with the pack's env files in `kindgi dev` (less any secret stored with `kindgi secrets set`, which a tool declares and reads from `ctx.secrets`, and any model provider's key, which no tool gets), and with the container's environment in an image. Declare the names your code reads in `kindgi.config.ts`, `env: { required: ['CITATOR_URL'], optional: [...] }`: a deployment injects exactly those, a pack service missing a required one isn't ready and says which, and `kindgi dev` warns about it. In an image the pack service also drops every variable the pack doesn't declare before your code loads (`kindgi dev` keeps them), so an undeclared name works locally and is unset once deployed: declare every name the code reads. Values per environment go in `environments.<name>.env`, secrets only as references.
|
|
174
|
-
|
|
175
|
-
### Connections to other apps
|
|
176
|
-
|
|
177
|
-
A tool that acts in another app as a person or as the company (sending mail, opening an issue) declares a **connection**, never a secret holding someone's token, and reads the call's short-lived token from `ctx.connections` (runtime 0.2.0 or later):
|
|
178
|
-
|
|
179
|
-
```ts
|
|
180
|
-
import { ConnectionRejectedError, defineTool } from '@kindgi/sdk/define';
|
|
181
|
-
|
|
182
|
-
const defined = defineTool({
|
|
183
|
-
// …id, description, version, input, output, effects…
|
|
184
|
-
needsSpec: {
|
|
185
|
-
connections: { mail: { app: 'google', permissions: ['gmail.send'], as: 'person' } },
|
|
186
|
-
},
|
|
187
|
-
handler: async (input, ctx) => {
|
|
188
|
-
const mail = ctx.connections?.mail;
|
|
189
|
-
const res = await fetch(url, { headers: { authorization: `Bearer ${mail?.accessToken}` } });
|
|
190
|
-
if (res.status === 401) throw new ConnectionRejectedError('mail');
|
|
191
|
-
// …
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
- **Whose connection:** `as: 'person'` is the connection of the person the run acts as (whoever started it, or the owner of the schedule or webhook that did); `as: 'company'` is the company's. The runtime picks it from the run, never from the input or the model. A run that acts as nobody (a service account's, an event's) fails a `'person'` call with `connection-needs-a-person`.
|
|
197
|
-
- **The token:** `accessToken` is current for this call only. Never keep, print, log or return it; it never reaches the journal. `account.label` says whose account it is (`ana@acme.com`).
|
|
198
|
-
- **A 401 from the app:** throw `new ConnectionRejectedError('mail')`, with the connection's name (a second argument can say what the app answered, never the token), or your own error with it as its `cause`. The runtime renews the token once and runs the call again; refused again, the call fails with `connection-rejected`.
|
|
199
|
-
- **An HTTP tool** sends the token with no code: `authorization: { kind: 'connection', connection: 'mail' }`.
|
|
200
|
-
- **Ready-made tools:** for Gmail, Outlook, GitHub and LinkedIn, include one from `@kindgi/connections` instead of writing it. Each is a factory, and the pack names its id; there's no default, because a tool id is the tenant's, filed under the first project that publishes it. A tool folder's `index.ts` is one line, `export default gmailSend({ id: 'acme.gmail.send' })` (`@kindgi/connections/google/tools`, with `gmailMessagesList`, `gmailMessageGet` and `gmailReply`); `githubIssuesList`, `githubLabelsList` and `githubIssuesAddLabels` come from `@kindgi/connections/github/tools`, `outlookSend`, `outlookMessagesList`, `outlookMessageGet` and `outlookReply` from `@kindgi/connections/microsoft/tools`, and `linkedinPost` from `@kindgi/connections/linkedin/tools`. Agents and approval settings use the pack's id. A mail tool acts in the person's own mailbox; `as: 'company'` makes it use the company's. `as` is part of what the tool declares, so an id you've published keeps its `as`: give a company mailbox its own tool id.
|
|
201
|
-
- **Your own calls to an app's API:** `appFetch(ctx, { entry, connection, toolId, url, method, headers, body })` from `@kindgi/connections/app-fetch` sends the connection's token as the only `Authorization`, paces calls by the entry's `rateLimits` (`toolId` is what its `costs` charge; 1 when not listed), tries a busy answer again, and throws a 401 as `ConnectionRejectedError`.
|
|
202
|
-
- **Unattended runs:** in a run no person started, a tool that uses an `as: 'person'` connection waits for that person's yes, reads included: `mutating: false` doesn't lift it. The agent's `conversationPolicy.hitl` can't lift it; only a trigger's `toolsAllowedUnattended` can, which a project admin sets, as for `needsApprovalWhenUnattended` (below). In a run a new mail started, the mail is a stranger's text, so two more calls wait: a call of a tool that isn't `mutating: false`, and, when the trigger reads the company's mailbox, a read with the company's connection to that app. The mail trigger's `toolsAllowedUnattended` lifts these too.
|
|
203
|
-
- **In a unit test:** `ctx.connections` is optional in the type; build a context with `toolContextForTest({ connections: { mail: { accessToken: 'test', account: { id: '1', label: 'ana@acme.com' }, … } } })` (`@kindgi/sdk/define`).
|
|
204
|
-
|
|
205
|
-
Docs: https://docs.kindgi.com/v0.2/guides/connections/use-a-connection/
|
|
206
|
-
|
|
207
|
-
## Declarative HTTP spec
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
// tools/fetch-order/index.ts
|
|
211
|
-
import { defineTool } from '@kindgi/sdk/define';
|
|
212
|
-
import type { ToolId } from '@kindgi/sdk/types';
|
|
213
|
-
import { z } from 'zod';
|
|
214
|
-
|
|
215
|
-
const defined = defineTool({
|
|
216
|
-
id: 'shop.fetch-order' as ToolId,
|
|
217
|
-
description: 'Fetch an order by id from the storefront API.',
|
|
218
|
-
version: '0.1.0',
|
|
219
|
-
input: z.object({ orderId: z.string() }),
|
|
220
|
-
output: z.object({ id: z.string(), status: z.string(), total: z.number() }),
|
|
221
|
-
effects: [],
|
|
222
|
-
mutating: false,
|
|
223
|
-
spec: {
|
|
224
|
-
kind: 'http',
|
|
225
|
-
method: 'GET',
|
|
226
|
-
urlTemplate: 'https://api.shop.example/orders/{orderId}',
|
|
227
|
-
// Every {placeholder} MUST be a key on the input schema
|
|
228
|
-
authorization: {
|
|
229
|
-
kind: 'bearer',
|
|
230
|
-
secretRef: { envName: 'production', name: 'shop-api-token' },
|
|
231
|
-
},
|
|
232
|
-
successStatus: { min: 200, max: 299 },
|
|
233
|
-
},
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
if (defined.kind === 'err') throw new Error(defined.error.message);
|
|
237
|
-
export default defined.value;
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
`timeoutMs` (default 30 seconds) covers the whole call, reading the response included, and so does a run's cancel. A response body over 10 MiB, counted after it's decompressed, fails the call. So point a tool at an endpoint that pages or filters, not one that returns everything.
|
|
241
|
-
|
|
242
|
-
## Read-only tools: `mutating`
|
|
243
|
-
|
|
244
|
-
`mutating: false` declares that the tool changes nothing outside Kindgi; it only reads. Leaving `mutating` out counts as mutating, the same as `true`. It decides three things, in both authoring modes:
|
|
245
|
-
|
|
246
|
-
- **Dry runs.** `kindgi runs start --dry-run` runs a tool only if it's `mutating: false` and its `effects` declare no `writes`, `deletes`, `spawns-run`, `emits-event` or `external-side-effect`. The first other tool stops the run with `dry-run-effectful-tool`.
|
|
247
|
-
- **Approval gates.** When an agent turns tool gates on (`conversationPolicy.hitl.tools`) and sets neither an override for the tool nor a `default`, a `mutating: false` tool runs straight through, and any other tool asks for approval on its first use.
|
|
248
|
-
- **Runs a new mail started.** A call of a tool that isn't `mutating: false` waits for a person's yes, unless the mail trigger's `toolsAllowedUnattended` lists it.
|
|
249
|
-
|
|
250
|
-
So declare `mutating: false` on every tool that only reads, and never on one that writes.
|
|
251
|
-
|
|
252
|
-
A tool that writes also says what it writes, in `effects`, beside `mutating: true`:
|
|
253
|
-
|
|
254
|
-
```ts
|
|
255
|
-
effects: [{ kind: 'writes', resource: 'acme:refunds' }],
|
|
256
|
-
mutating: true,
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
The kinds are `reads`, `writes`, `deletes`, `network`, `spawns-run`, `emits-event`, `external-side-effect` and `sensitive-data-egress` (`EFFECT_KINDS` in `@kindgi/tools`); `defineTool` refuses any other. `resource` is free text naming what the tool touches. A read-only tool keeps `effects: []`.
|
|
260
|
-
|
|
261
|
-
## A person's yes when no one is watching: `needsApprovalWhenUnattended`
|
|
262
|
-
|
|
263
|
-
`needsApprovalWhenUnattended: true` declares that a person must say yes to every call of the tool in a run no person started: one a schedule, a webhook, a new mail, an event or a service account's key started. In such a run:
|
|
264
|
-
|
|
265
|
-
- **An agent's call waits** for a person's yes, whatever the agent's `conversationPolicy.hitl.tools` says. The agent's rules can't lift it: agents are reused, and the tool's author knows what it does.
|
|
266
|
-
- **A flow's tool step that calls it is refused.** Put the tool behind an agent step instead, whose call waits.
|
|
267
|
-
- **A trigger can lift both, for its own runs only.** A project admin names the tool in a webhook trigger's, a schedule's or a mail trigger's `toolsAllowedUnattended` (`--allow-unattended=<tool-id>` on `kindgi webhooks`, `kindgi schedules` and `kindgi mail-triggers`). In that trigger's runs, the tool is then as in a run a person started. The tenant's approval rules still apply.
|
|
268
|
-
|
|
269
|
-
In a run a person started (signed in, or with their own API key), the agent's rules decide, as for any tool. Absent means `false`. Declare it on a tool that moves money or changes something people rely on:
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
effects: [{ kind: 'writes', resource: 'acme:refunds' }],
|
|
273
|
-
mutating: true,
|
|
274
|
-
needsApprovalWhenUnattended: true,
|
|
275
|
-
```
|
|
276
|
-
|
|
277
|
-
Docs: https://docs.kindgi.com/v0.2/guides/approvals/ask-before-a-tool-runs/#when-no-person-started-the-run
|
|
278
|
-
|
|
279
|
-
A step can run more than once (resumed after an approval, retried after a failure, run again after a crash), so a tool that writes uses `ctx.idempotencyKey`: the same every time this call runs, different for every other call. Pass it to the system you write to (an `Idempotency-Key` header, a client reference, a unique column) or look for it there first, and a refund never goes out twice. Not `ctx.requestId`: a model's call id is only unique within one of its answers. The key is absent outside a run and from a runtime before 0.1.6. Docs: https://docs.kindgi.com/v0.2/guides/tools/write-a-tool/#make-a-side-effect-happen-once
|
|
280
|
-
|
|
281
|
-
## Results that are text from outside: `resultIsOutsideData`
|
|
282
|
-
|
|
283
|
-
`resultIsOutsideData: true` says the tool's results are text from outside that nobody checked, such as a mail someone sent or a page a search found. An agent reads each result of the tool as data, not instructions, in that turn and in every later turn's history. It changes what the model is told, not what the tool may do: pair it with approvals for anything an agent could do with what it read. Absent means `false`.
|
|
65
|
+
| Your case | Read |
|
|
66
|
+
|---|---|
|
|
67
|
+
| A `handler`: what it gets (`input`, `ctx`), and logging | [imperative-handler.md](imperative-handler.md) |
|
|
68
|
+
| An env value, a tenant's secret, or acting in another app (a connection) | [secrets-and-connections.md](secrets-and-connections.md) |
|
|
69
|
+
| A plain remote call: `spec: {kind: 'http'}` | [declarative-http.md](declarative-http.md) |
|
|
70
|
+
| A read-only tool, a person's yes in unattended runs, results that are outside text | [tool-flags.md](tool-flags.md) |
|
|
71
|
+
| Changing a tool, its `version`, its tests, code several tools share | [iterating-and-testing.md](iterating-and-testing.md) |
|
|
72
|
+
| An error, or a tool that won't index or load | [common-mistakes.md](common-mistakes.md) |
|
|
284
73
|
|
|
285
74
|
## Tool id convention
|
|
286
75
|
|
|
@@ -289,50 +78,6 @@ prefix scopes the tool to its pack; `<tool-name>` names the operation.
|
|
|
289
78
|
Enforced by `defineTool` at author time. Examples:
|
|
290
79
|
`acme.verify-citation`, `shop.fetch-order`, `demo.echo`.
|
|
291
80
|
|
|
292
|
-
## Iterating on a tool
|
|
293
|
-
|
|
294
|
-
Edit the source file (`tools/<tool>/index.ts`), save. The next
|
|
295
|
-
`kindgi runs start` sees the change — new input/output schema, new
|
|
296
|
-
description, new handler behavior. No version bump, no restart, no
|
|
297
|
-
re-registration ceremony. Source is truth in dev.
|
|
298
|
-
|
|
299
|
-
The `version` field is a **semver contract for humans** — it declares
|
|
300
|
-
what callers can rely on. Bump it because the *contract with
|
|
301
|
-
downstream callers* changed (removed a field, tightened a type,
|
|
302
|
-
narrowed enum values), not because you saved the file. If you're
|
|
303
|
-
iterating in dev and the shape isn't finalized, leave `version`
|
|
304
|
-
alone.
|
|
305
|
-
|
|
306
|
-
**When version matters:** `kindgi deploy` publishes to a durable
|
|
307
|
-
production registry that enforces the immutable `(id, version)`
|
|
308
|
-
contract — a re-publish of the same version with different bytes
|
|
309
|
-
gets rejected. That's when semver discipline kicks in. The deploy
|
|
310
|
-
tooling surfaces the check; you don't have to think about it while
|
|
311
|
-
authoring.
|
|
312
|
-
|
|
313
|
-
**Downstream callers still pin ranges.** An agent's `tools: [{id,
|
|
314
|
-
version: '^0.1.0'}]` picks the highest active version matching the
|
|
315
|
-
range at run start. Compatible tool updates (patch, minor) reach the
|
|
316
|
-
agent without editing agent source; breaking updates (major) require
|
|
317
|
-
the agent-author to opt in.
|
|
318
|
-
|
|
319
|
-
## Testing a tool
|
|
320
|
-
|
|
321
|
-
Put a tool's tests beside it, `tools/<tool>/index.test.ts`. Discovery skips `*.test.*` and `*.spec.*` files (`.ts`, `.js`, `.mjs`, `.cjs`), so the indexer never loads a test as a primitive: don't move tests elsewhere to keep them out. `invokeTool(tool, input, ctx)` from `@kindgi/sdk/define` calls the tool the way Kindgi does, schemas included, and returns a `Result`. `toolContextForTest()` builds its `ctx` (tenant `tenant-test`, run `run-test`, an abort signal) and takes what to change: `env`, `secrets`, `projectId`.
|
|
322
|
-
|
|
323
|
-
```ts
|
|
324
|
-
import { invokeTool, toolContextForTest } from '@kindgi/sdk/define';
|
|
325
|
-
|
|
326
|
-
const result = await invokeTool(lookupOrder, { orderId: 'ord_1001' }, toolContextForTest());
|
|
327
|
-
// a tool with env values: invokeToolForTest(tool, input, { env: { STORE_URL: '…' } })
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
`kindgi test` runs the pack's tests with vitest (`vitest run`; `--watch` keeps watching). In an app Kindgi was added to, `kindgi init` wrote the vitest config (`vitest.config.mts`, or `.ts` in a `"type": "module"` app): it runs `kindgi/**/*.test.ts`, and its `resolve.alias` copies `tsconfig.json`'s `paths`, which vitest doesn't read. A test that can't resolve an alias the app's code uses (`@/…`) means the two differ: update the config's alias, not the import.
|
|
331
|
-
|
|
332
|
-
## Shared code
|
|
333
|
-
|
|
334
|
-
Code several tools share (schemas, a client, helpers) goes outside `tools/`, for example in `lib/` beside it. Discovery loads every `.ts`, `.js` and `.mjs` file under `tools/` (tests aside) as a primitive, so a helper there fails the index. Don't put it in the app's own source either: import the app's functions from where they are (unless their module imports `server-only`: see mistake 12; for a CommonJS package's default import, see mistake 13), and keep what's Kindgi's in the pack's folder.
|
|
335
|
-
|
|
336
81
|
## Wiring the tool onto an agent
|
|
337
82
|
|
|
338
83
|
Agents reference tools via `ToolRef[]`, NOT `string[]`. Each entry is
|
|
@@ -349,132 +94,12 @@ tools: [
|
|
|
349
94
|
The resolver picks the highest active version matching the range at
|
|
350
95
|
run start via `semver.maxSatisfying`. No implicit `:latest`.
|
|
351
96
|
|
|
352
|
-
## Common mistakes
|
|
353
|
-
|
|
354
|
-
1. **Copying another tool's shape without user intent.** The pack may
|
|
355
|
-
ship `tools/echo/` as a starter example. Copying its skeleton to
|
|
356
|
-
make `tools/lookup/` produces plausible-looking code that solves
|
|
357
|
-
the wrong problem. Ask the user what the new tool should DO first.
|
|
358
|
-
|
|
359
|
-
2. **Passing bare strings to `agent.tools`.** `tools: ['acme.verify-citation']`
|
|
360
|
-
doesn't type-check (`tools` is `ToolRef[]`), and `defineAgent`
|
|
361
|
-
returns `invalid-agent` for a bare string that slips through. Use
|
|
362
|
-
`[{id: 'acme.verify-citation', version: '^0.1.0'}]`.
|
|
363
|
-
|
|
364
|
-
3. **Forgetting the `Result` unwrap.** `defineTool` returns
|
|
365
|
-
`Result<DefinedTool, ToolError>` — a bad schema doesn't throw at import
|
|
366
|
-
time unless you check `defined.kind === 'err'`. Always unwrap at
|
|
367
|
-
module load so a broken pack fails LOUDLY, not on first invocation.
|
|
368
|
-
|
|
369
|
-
4. **`handler` and `spec` together.** Mutually exclusive.
|
|
370
|
-
`defineTool` returns `invalid-tool-definition` with both set.
|
|
371
|
-
|
|
372
|
-
5. **`{placeholder}` in `urlTemplate` without a matching input key.**
|
|
373
|
-
HTTP tools substitute placeholders from the input at invoke time.
|
|
374
|
-
Missing keys throw a clear error at invoke time — check both sides
|
|
375
|
-
line up.
|
|
376
|
-
|
|
377
|
-
6. **An HTTP tool's `secretRef` in the wrong env.** The runtime
|
|
378
|
-
resolves `authorization.secretRef` itself, on every call, in the env
|
|
379
|
-
the ref names: `{ envName: 'local', … }` is the pack's `.env` /
|
|
380
|
-
`.env.local` under `kindgi dev`, while `'production'` reads
|
|
381
|
-
`.env.production`. A secret it can't find fails the call with
|
|
382
|
-
`secret-unavailable`, naming it. Only a harness of your own that
|
|
383
|
-
calls `invokeTool` directly has to pass `ctx.resolveSecret`.
|
|
384
|
-
7. **Reading a secret from `process.env` in a tool that runs for many
|
|
385
|
-
tenants.** The pack service's environment is one for all of them.
|
|
386
|
-
Declare the secret in `needsSpec.secrets` and read `ctx.secrets`.
|
|
387
|
-
|
|
388
|
-
8. **Bumping `version` on every dev save.** Old habit from
|
|
389
|
-
frameworks that stored manifests immutably by `(id, version)`.
|
|
390
|
-
Kindgi's dev mode reads tool manifests directly from source —
|
|
391
|
-
version doesn't gate iteration. Bump it when the *contract*
|
|
392
|
-
changes (breaking schema shape, semantic behavior), not when you
|
|
393
|
-
save. See the "Iterating on a tool" section above.
|
|
394
|
-
|
|
395
|
-
9. **A package a tool imports, listed only in `devDependencies`.** The
|
|
396
|
-
deployed pack installs the app's production dependencies only, so the
|
|
397
|
-
import works under `kindgi dev` and fails in the image. When a tool
|
|
398
|
-
imports a new package (an ORM client such as `@prisma/client`, an API
|
|
399
|
-
SDK), check that the app's `package.json` lists it under
|
|
400
|
-
`dependencies`. Build-time tools (the `prisma` CLI, `typescript`) stay
|
|
401
|
-
in `devDependencies`. `kindgi dev` warns as soon as a tool imports one
|
|
402
|
-
("⚠ The pack imports @prisma/client (in kindgi/tools/…), which
|
|
403
|
-
package.json lists only in devDependencies: …"), and `kindgi build`
|
|
404
|
-
refuses the pack until it moves.
|
|
405
|
-
10. **A tool that needs the app's install scripts in the image.** The
|
|
406
|
-
image installs with scripts off, so the app's `postinstall` /
|
|
407
|
-
`prepare` (`prisma generate`, husky) don't run there; `kindgi build`
|
|
408
|
-
lists them ("✓ The app's own install scripts don't run in the image:
|
|
409
|
-
…"). A tool that uses Prisma's client then fails the build ("@prisma/client
|
|
410
|
-
did not initialize yet"). Add `prisma({ schema: 'prisma/schema.prisma' })`
|
|
411
|
-
(from `@kindgi/sdk/build`; add `config: 'prisma.config.ts'` when the app
|
|
412
|
-
has one) to `image.extensions` in `kindgi.config.ts`. Other generate
|
|
413
|
-
steps: `defineBuildExtension({ name, contextFiles, postInstall: [{ bin, args }] })`.
|
|
414
|
-
Debian packages: `image.systemPackages`. Placeholder env for those steps:
|
|
415
|
-
`image.buildEnv` (never secrets).
|
|
416
|
-
|
|
417
|
-
11. **Turning the dev sandbox off for a file the tool needs.** Under `kindgi dev`
|
|
418
|
-
the tool's code runs sandboxed: it can't read outside the app (the home
|
|
419
|
-
folder, `~/.aws`) or any `.env*` file, and gets `EPERM` there. A tool that
|
|
420
|
-
needs a path names it in `kindgi.config.ts`: `dev: { sandbox: { allowRead: ['~/.aws'] } }`.
|
|
421
|
-
Never set `KINDGI_DEV_SANDBOX=off` or `dev.sandbox: false` to get past it
|
|
422
|
-
(https://docs.kindgi.com/v0.2/guides/secrets/dev-sandbox/).
|
|
423
|
-
|
|
424
|
-
12. **Importing a Next.js module that has `import 'server-only'`.** Outside
|
|
425
|
-
Next.js that line stops the module from loading, so the tool fails
|
|
426
|
-
`kindgi test`, `kindgi dev` and `kindgi build`. When the app doesn't
|
|
427
|
-
install the package (Next.js doesn't need it), `kindgi test` fails with
|
|
428
|
-
`Cannot find package 'server-only'`, and `kindgi dev` and `kindgi build`
|
|
429
|
-
with `Could not resolve "server-only"`. When it does, all three fail
|
|
430
|
-
with the package's own "This module cannot be imported from a Client
|
|
431
|
-
Component module". Don't remove the line, and don't alias `server-only`
|
|
432
|
-
away in the vitest config: the build still fails. Move the code the tool
|
|
433
|
-
needs, as it is, to a module without the line (`lib/orders-core.ts`).
|
|
434
|
-
The guarded module keeps the line and re-exports it
|
|
435
|
-
(`import 'server-only'; export * from './orders-core';`), so the app's
|
|
436
|
-
own imports don't change, and the tool imports the new module. Nothing
|
|
437
|
-
guards it, so only the app's server code and the pack should import it.
|
|
438
|
-
|
|
439
|
-
13. **A default import of a package that ships only CommonJS.** A pack's
|
|
440
|
-
code runs as ES modules: `kindgi build` bundles it into `.mjs` files,
|
|
441
|
-
leaving out the packages from `node_modules`, and `kindgi dev` and the
|
|
442
|
-
deployed pack load it with Node's own `import()`. Node gives
|
|
443
|
-
`import OrdersClient from '@acme/orders-client'` the package's whole
|
|
444
|
-
`module.exports`; when the package puts its class on `exports.default`
|
|
445
|
-
(TypeScript's CommonJS output for `export default`), the class is on its
|
|
446
|
-
`default`. Vitest, Next.js, and tsx in an app without `"type": "module"`
|
|
447
|
-
give the class itself. So the tool passes `kindgi test`, and app code it
|
|
448
|
-
imports works in the app, but under `kindgi dev` and once deployed it
|
|
449
|
-
fails with `TypeError: OrdersClient is not a constructor`. With
|
|
450
|
-
`"moduleResolution": "NodeNext"` (a new pack's `tsconfig.json`),
|
|
451
|
-
TypeScript already refuses `new OrdersClient()`; with `"bundler"`
|
|
452
|
-
(Next.js apps) it compiles. This is Node's own behavior, and Kindgi keeps
|
|
453
|
-
it: take the class either way.
|
|
454
|
-
|
|
455
|
-
```ts
|
|
456
|
-
import OrdersClientExport from '@acme/orders-client';
|
|
457
|
-
|
|
458
|
-
type DefaultOf<T> = T extends { default: infer C } ? C : T;
|
|
459
|
-
|
|
460
|
-
function defaultOf<T>(loaded: T): DefaultOf<T> {
|
|
461
|
-
const value = typeof loaded === 'function' ? loaded : (loaded as { default: unknown }).default;
|
|
462
|
-
return value as DefaultOf<T>;
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const OrdersClient = defaultOf(OrdersClientExport);
|
|
466
|
-
```
|
|
467
|
-
|
|
468
|
-
A dynamic import is the same: `(await import('@acme/orders-client')).default`
|
|
469
|
-
is the class under Vitest and the whole `module.exports` under Node, so
|
|
470
|
-
take it with `defaultOf((await import('@acme/orders-client')).default)`.
|
|
471
|
-
|
|
472
97
|
## References
|
|
473
98
|
|
|
474
99
|
- Type surface: `hover any @kindgi/sdk/define export` in your editor
|
|
475
100
|
for full JSDoc — every field on `DefineToolSpec` / `ToolManifest`
|
|
476
101
|
documents purpose, when to set it, and gotchas.
|
|
477
|
-
- API reference: https://docs.kindgi.com/v0.
|
|
102
|
+
- API reference: https://docs.kindgi.com/v0.3/reference/typescript/sdk/kindgi/sdk/define/ (every `define*` spec, field by field).
|
|
478
103
|
- Common patterns: check the `sample` template (`kindgi init
|
|
479
104
|
--template=sample`) for working examples of both authoring modes.
|
|
480
105
|
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Common mistakes
|
|
2
|
+
|
|
3
|
+
## Contents
|
|
4
|
+
|
|
5
|
+
- 1. Copying another tool's shape without user intent.
|
|
6
|
+
- 2. Passing bare strings to `agent.tools`.
|
|
7
|
+
- 3. Forgetting the `Result` unwrap.
|
|
8
|
+
- 4. `handler` and `spec` together.
|
|
9
|
+
- 5. `{placeholder}` in `urlTemplate` without a matching input key.
|
|
10
|
+
- 6. An HTTP tool's `secretRef` in the wrong env.
|
|
11
|
+
- 7. Reading a secret from `process.env` in a tool that runs for many tenants.
|
|
12
|
+
- 8. Bumping `version` on every dev save.
|
|
13
|
+
- 9. A package a tool imports, listed only in `devDependencies`.
|
|
14
|
+
- 10. A tool that needs the app's install scripts in the image.
|
|
15
|
+
- 11. Turning the dev sandbox off for a file the tool needs.
|
|
16
|
+
- 12. Importing a Next.js module that has `import 'server-only'`.
|
|
17
|
+
- 13. A default import of a package that ships only CommonJS.
|
|
18
|
+
|
|
19
|
+
1. **Copying another tool's shape without user intent.** The pack may
|
|
20
|
+
ship `tools/echo/` as a starter example. Copying its skeleton to
|
|
21
|
+
make `tools/lookup/` produces plausible-looking code that solves
|
|
22
|
+
the wrong problem. Ask the user what the new tool should DO first.
|
|
23
|
+
|
|
24
|
+
2. **Passing bare strings to `agent.tools`.** `tools: ['acme.verify-citation']`
|
|
25
|
+
doesn't type-check (`tools` is `ToolRef[]`), and `defineAgent`
|
|
26
|
+
returns `invalid-agent` for a bare string that slips through. Use
|
|
27
|
+
`[{id: 'acme.verify-citation', version: '^0.1.0'}]`.
|
|
28
|
+
|
|
29
|
+
3. **Forgetting the `Result` unwrap.** `defineTool` returns
|
|
30
|
+
`Result<DefinedTool, ToolError>` — a bad schema doesn't throw at import
|
|
31
|
+
time unless you check `defined.kind === 'err'`. Always unwrap at
|
|
32
|
+
module load so a broken pack fails LOUDLY, not on first invocation.
|
|
33
|
+
|
|
34
|
+
4. **`handler` and `spec` together.** Mutually exclusive.
|
|
35
|
+
`defineTool` returns `invalid-tool-definition` with both set.
|
|
36
|
+
|
|
37
|
+
5. **`{placeholder}` in `urlTemplate` without a matching input key.**
|
|
38
|
+
HTTP tools substitute placeholders from the input at invoke time.
|
|
39
|
+
Missing keys throw a clear error at invoke time — check both sides
|
|
40
|
+
line up.
|
|
41
|
+
|
|
42
|
+
6. **An HTTP tool's `secretRef` in the wrong env.** The runtime
|
|
43
|
+
resolves `authorization.secretRef` itself, on every call, in the env
|
|
44
|
+
the ref names: `{ envName: 'local', … }` is the pack's `.env` /
|
|
45
|
+
`.env.local` under `kindgi dev`, while `'production'` reads
|
|
46
|
+
`.env.production`. A secret it can't find fails the call with
|
|
47
|
+
`secret-unavailable`, naming it. Only a harness of your own that
|
|
48
|
+
calls `invokeTool` directly has to pass `ctx.resolveSecret`.
|
|
49
|
+
7. **Reading a secret from `process.env` in a tool that runs for many
|
|
50
|
+
tenants.** The pack service's environment is one for all of them.
|
|
51
|
+
Declare the secret in `needsSpec.secrets` and read `ctx.secrets`.
|
|
52
|
+
|
|
53
|
+
8. **Bumping `version` on every dev save.** Old habit from
|
|
54
|
+
frameworks that stored manifests immutably by `(id, version)`.
|
|
55
|
+
Kindgi's dev mode reads tool manifests directly from source —
|
|
56
|
+
version doesn't gate iteration. Bump it when the *contract*
|
|
57
|
+
changes (breaking schema shape, semantic behavior), not when you
|
|
58
|
+
save. See `iterating-and-testing.md`.
|
|
59
|
+
|
|
60
|
+
9. **A package a tool imports, listed only in `devDependencies`.** The
|
|
61
|
+
deployed pack installs the app's production dependencies only, so the
|
|
62
|
+
import works under `kindgi dev` and fails in the image. When a tool
|
|
63
|
+
imports a new package (an ORM client such as `@prisma/client`, an API
|
|
64
|
+
SDK), check that the app's `package.json` lists it under
|
|
65
|
+
`dependencies`. Build-time tools (the `prisma` CLI, `typescript`) stay
|
|
66
|
+
in `devDependencies`. `kindgi dev` warns as soon as a tool imports one
|
|
67
|
+
("⚠ The pack imports @prisma/client (in kindgi/tools/…), which
|
|
68
|
+
package.json lists only in devDependencies: …"), and `kindgi build`
|
|
69
|
+
refuses the pack until it moves.
|
|
70
|
+
10. **A tool that needs the app's install scripts in the image.** The
|
|
71
|
+
image installs with scripts off, so the app's `postinstall` /
|
|
72
|
+
`prepare` (`prisma generate`, husky) don't run there; `kindgi build`
|
|
73
|
+
lists them ("✓ The app's own install scripts don't run in the image:
|
|
74
|
+
…"). A tool that uses Prisma's client then fails the build ("@prisma/client
|
|
75
|
+
did not initialize yet"). Add `prisma({ schema: 'prisma/schema.prisma' })`
|
|
76
|
+
(from `@kindgi/sdk/build`; add `config: 'prisma.config.ts'` when the app
|
|
77
|
+
has one) to `image.extensions` in `kindgi.config.ts`. Other generate
|
|
78
|
+
steps: `defineBuildExtension({ name, contextFiles, postInstall: [{ bin, args }] })`.
|
|
79
|
+
Debian packages: `image.systemPackages`. Placeholder env for those steps:
|
|
80
|
+
`image.buildEnv` (never secrets).
|
|
81
|
+
|
|
82
|
+
11. **Turning the dev sandbox off for a file the tool needs.** Under `kindgi dev`
|
|
83
|
+
the tool's code runs sandboxed: it can't read outside the app (the home
|
|
84
|
+
folder, `~/.aws`) or any `.env*` file, and gets `EPERM` there. A tool that
|
|
85
|
+
needs a path names it in `kindgi.config.ts`: `dev: { sandbox: { allowRead: ['~/.aws'] } }`.
|
|
86
|
+
Never set `KINDGI_DEV_SANDBOX=off` or `dev.sandbox: false` to get past it
|
|
87
|
+
(https://docs.kindgi.com/v0.3/guides/secrets/dev-sandbox/).
|
|
88
|
+
|
|
89
|
+
12. **Importing a Next.js module that has `import 'server-only'`.** Outside
|
|
90
|
+
Next.js that line stops the module from loading, so the tool fails
|
|
91
|
+
`kindgi test`, `kindgi dev` and `kindgi build`. When the app doesn't
|
|
92
|
+
install the package (Next.js doesn't need it), `kindgi test` fails with
|
|
93
|
+
`Cannot find package 'server-only'`, and `kindgi dev` and `kindgi build`
|
|
94
|
+
with `Could not resolve "server-only"`. When it does, all three fail
|
|
95
|
+
with the package's own "This module cannot be imported from a Client
|
|
96
|
+
Component module". Don't remove the line, and don't alias `server-only`
|
|
97
|
+
away in the vitest config: the build still fails. Move the code the tool
|
|
98
|
+
needs, as it is, to a module without the line (`lib/orders-core.ts`).
|
|
99
|
+
The guarded module keeps the line and re-exports it
|
|
100
|
+
(`import 'server-only'; export * from './orders-core';`), so the app's
|
|
101
|
+
own imports don't change, and the tool imports the new module. Nothing
|
|
102
|
+
guards it, so only the app's server code and the pack should import it.
|
|
103
|
+
|
|
104
|
+
13. **A default import of a package that ships only CommonJS.** A pack's
|
|
105
|
+
code runs as ES modules: `kindgi build` bundles it into `.mjs` files,
|
|
106
|
+
leaving out the packages from `node_modules`, and `kindgi dev` and the
|
|
107
|
+
deployed pack load it with Node's own `import()`. Node gives
|
|
108
|
+
`import OrdersClient from '@acme/orders-client'` the package's whole
|
|
109
|
+
`module.exports`; when the package puts its class on `exports.default`
|
|
110
|
+
(TypeScript's CommonJS output for `export default`), the class is on its
|
|
111
|
+
`default`. Vitest, Next.js, and tsx in an app without `"type": "module"`
|
|
112
|
+
give the class itself. So the tool passes `kindgi test`, and app code it
|
|
113
|
+
imports works in the app, but under `kindgi dev` and once deployed it
|
|
114
|
+
fails with `TypeError: OrdersClient is not a constructor`. With
|
|
115
|
+
`"moduleResolution": "NodeNext"` (a new pack's `tsconfig.json`),
|
|
116
|
+
TypeScript already refuses `new OrdersClient()`; with `"bundler"`
|
|
117
|
+
(Next.js apps) it compiles. This is Node's own behavior, and Kindgi keeps
|
|
118
|
+
it: take the class either way.
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import OrdersClientExport from '@acme/orders-client';
|
|
122
|
+
|
|
123
|
+
type DefaultOf<T> = T extends { default: infer C } ? C : T;
|
|
124
|
+
|
|
125
|
+
function defaultOf<T>(loaded: T): DefaultOf<T> {
|
|
126
|
+
const value = typeof loaded === 'function' ? loaded : (loaded as { default: unknown }).default;
|
|
127
|
+
return value as DefaultOf<T>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const OrdersClient = defaultOf(OrdersClientExport);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
A dynamic import is the same: `(await import('@acme/orders-client')).default`
|
|
134
|
+
is the class under Vitest and the whole `module.exports` under Node, so
|
|
135
|
+
take it with `defaultOf((await import('@acme/orders-client')).default)`.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Declarative HTTP spec
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
// tools/fetch-order/index.ts
|
|
5
|
+
import { defineTool } from '@kindgi/sdk/define';
|
|
6
|
+
import type { ToolId } from '@kindgi/sdk/types';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
const defined = defineTool({
|
|
10
|
+
id: 'shop.fetch-order' as ToolId,
|
|
11
|
+
description: 'Fetch an order by id from the storefront API.',
|
|
12
|
+
version: '0.1.0',
|
|
13
|
+
input: z.object({ orderId: z.string() }),
|
|
14
|
+
output: z.object({ id: z.string(), status: z.string(), total: z.number() }),
|
|
15
|
+
effects: [],
|
|
16
|
+
mutating: false,
|
|
17
|
+
spec: {
|
|
18
|
+
kind: 'http',
|
|
19
|
+
method: 'GET',
|
|
20
|
+
urlTemplate: 'https://api.shop.example/orders/{orderId}',
|
|
21
|
+
// Every {placeholder} MUST be a key on the input schema
|
|
22
|
+
authorization: {
|
|
23
|
+
kind: 'bearer',
|
|
24
|
+
secretRef: { envName: 'production', name: 'shop-api-token' },
|
|
25
|
+
},
|
|
26
|
+
successStatus: { min: 200, max: 299 },
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
if (defined.kind === 'err') throw new Error(defined.error.message);
|
|
31
|
+
export default defined.value;
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`timeoutMs` (default 30 seconds) covers the whole call, reading the response included, and so does a run's cancel. A response body over 10 MiB, counted after it's decompressed, fails the call. So point a tool at an endpoint that pages or filters, not one that returns everything.
|