Skip to content

fraction12/agenttk

Repository files navigation

AgentTK

npm version Verify license: MIT

AgentTK is a small TypeScript toolkit for building deterministic, agent-friendly CLIs. It is a framework for agent-authored CLIs, not a built-in CLI generator.

It gives you a clean core for tools that need:

  • deterministic command dispatch and built-in help
  • structured success and failure envelopes
  • JSON-first output for agents
  • concise human output for operators
  • reusable auth, lookup, adapter, and config patterns
  • dry-run semantics for mutation commands
  • lightweight test helpers for CLI behavior

What it is

AgentTK is a narrow toolkit for building reliable CLIs with a stable machine-facing contract.

It is designed for a workflow like this:

  1. an agent reads a skill, API, or integration contract
  2. the agent writes the actual CLI code
  3. AgentTK supplies the reusable plumbing so the tool does not have to reinvent it

Use it when you want:

  • predictable command dispatch with alias and help support
  • machine-readable results without string parsing
  • simple human output layered on top of the same result envelope
  • reusable framework blocks for auth, lookup, config, and adapter contracts
  • small test helpers instead of shell-heavy CLI tests

What it is not

AgentTK intentionally does not include:

  • a built-in CLI generator
  • plugin systems
  • workflow engines
  • auth doctor flows
  • provenance helpers
  • dynamic runtime loading
  • provider SDK wrappers
  • domain-specific adapters

Product boundary

AgentTK owns

  • runtime and command structure
  • help metadata and rendering
  • result envelopes
  • validation and corrective guidance
  • dry-run semantics
  • auth, lookup, adapter, and config primitives
  • test helpers

Your agent owns

  • reading the skill or API contract
  • deciding the command surface
  • writing the actual tool code
  • composing the AgentTK primitives correctly

The downstream tool repo owns

  • provider integrations
  • business rules
  • safety constraints
  • domain quirks
  • live operational behavior

Install

npm install agenttk

If you want schema validation:

npm install zod

Example

import { defineCommand, ok, runToolCli } from 'agenttk'

await runToolCli({
  name: 'demo',
  description: 'Example CLI built with AgentTK',
  commands: [
    defineCommand({
      name: 'hello',
      description: 'Say hello',
      aliases: ['hi'],
      usage: 'demo hello [name]',
      examples: ['demo hello', 'demo hi Dushyant'],
      handler: async ({ rawArgs }) =>
        ok({
          type: 'greeting',
          record: { message: `hello, ${rawArgs[0] ?? 'world'}` }
        })
    })
  ]
})

runToolCli(...) is the recommended thin entrypoint for downstream CLIs. It handles command dispatch, aliases, help, JSON mode, and stdout/stderr emission so your package entry file can stay focused on command definitions.

Validation + dry-run

import { z } from 'zod'
import { asDryRun, createTool, defineCommand, isFailure, ok, validateInput } from 'agenttk'

const schema = z.object({
  title: z.string().min(1, 'title is required'),
  dryRun: z.boolean().default(false)
})

const tool = createTool({
  name: 'tasks',
  commands: [
    defineCommand({
      name: 'add',
      handler: async () => {
        const parsed = validateInput(schema, { title: '', dryRun: true }, {
          nextStep: 'Run tasks add --title "Send estimate" [--dry-run]'
        })

        if (isFailure(parsed)) return parsed

        const result = ok({
          type: 'task',
          id: 'task-123',
          destination: 'demo_tasks',
          record: { title: parsed.title }
        })

        return parsed.dryRun ? asDryRun(result) : result
      }
    })
  ]
})

API

Core runtime

  • createTool
  • runToolCli
  • defineCommand
  • ok
  • fail
  • isFailure
  • renderResult

Recovery semantics

  • withRecovery

Mutation safety

  • withMutationSafety
  • markPartial
  • markVerified
  • markUnverified
  • verifiedMutation
  • unverifiedMutation

Runtime kit helpers

  • hasFlag
  • firstPositional
  • invalidInput
  • operationalFailure
  • lockedOrBusy
  • confirmationRequiredResult

Validation and command behavior

  • validateInput
  • validationError
  • expectedPayloadShape
  • nextStepGuidance
  • asDryRun

Auth blocks

  • authRequired
  • authInvalid
  • accountMismatch
  • requireAuth

Lookup resolution

  • notFound
  • ambiguousMatch
  • resolveById
  • resolveByQuery
  • resolveOne

Risk and confirmation

  • defineRisk
  • confirmationRequired
  • requireConfirmation

Adapter contracts

  • defineAdapter
  • supportsCapability
  • requireCapability
  • adapterFailure
  • unsupportedCapability

Config conventions

  • defineProfile
  • loadConfig
  • validateConfig
  • missingConfig
  • malformedConfig
  • selectProfile

Testing helpers

  • runTool
  • expectOk
  • expectFailure
  • expectDryRun
  • expectAuthFailure
  • expectLookupFailure
  • expectAdapterFailure
  • expectConfigFailure
  • expectRecovery
  • expectMutationSafety
  • agentSafeCliChecklist
  • getAgentSafeCliChecklist
  • authFailureFixture
  • lookupCandidatesFixture
  • fakeAdapter

Auth preflight

import { createTool, defineCommand, ok, requireAuth } from 'agenttk'

const tool = createTool({
  name: 'calendar',
  commands: [
    defineCommand({
      name: 'sync',
      handler: async () => {
        const auth = await requireAuth(async () => ({
          ok: false,
          code: 'AUTH_REQUIRED',
          provider: 'google',
          nextStep: 'Run calendar auth login'
        }))

        if (auth !== true) return auth

        return ok({ type: 'sync', record: { status: 'started' } })
      }
    })
  ]
})

Adapter contracts

import {
  adapterFailure,
  createTool,
  defineAdapter,
  defineCommand,
  requireCapability
} from 'agenttk'

const adapter = defineAdapter({
  provider: 'google',
  capabilities: ['tasks.read']
})

const tool = createTool({
  name: 'tasks',
  commands: [
    defineCommand({
      name: 'add',
      handler: async () => {
        const capability = requireCapability(adapter, 'tasks.write', {
          operation: 'createTask',
          nextStep: 'Reconnect with write scopes'
        })

        if (capability !== true) return capability

        return adapterFailure('Google API timed out', {
          provider: 'google',
          operation: 'createTask',
          category: 'timeout',
          retryable: true,
          nextStep: 'Retry in a few seconds'
        })
      }
    })
  ]
})

Provider-specific SDK calls and object models should stay in the downstream repo. AgentTK only owns the contract boundary, capability checks, and normalized failure shapes.

Config conventions

import { createTool, defineCommand, isFailure, loadConfig, ok } from 'agenttk'
import { z } from 'zod'

const schema = z.object({
  account: z.string().min(1),
  apiBaseUrl: z.string().url()
})

const config = loadConfig(schema, {
  env: { apiBaseUrl: process.env.API_BASE_URL },
  profiles: {
    work: { account: 'team@example.com', apiBaseUrl: 'https://api.example.com' }
  },
  profile: 'work'
}, {
  expected: '{"account":"team@example.com","apiBaseUrl":"https://api.example.com"}',
  nextStep: 'Set API_BASE_URL or choose a valid profile'
})

if (isFailure(config)) return config
return ok({ type: 'config', record: config })

Load order is predictable: base config, then selected profile, then env overrides. AgentTK stays out of secrets management and provider-specific config policy.

Lookup resolution

import { ambiguousMatch, createTool, defineCommand, notFound, ok, resolveOne } from 'agenttk'

const tool = createTool({
  name: 'tasks',
  commands: [
    defineCommand({
      name: 'pick',
      handler: async () => {
        const matches = [
          { id: 'task-1', label: 'Invoice follow-up', description: 'Daily Focus' },
          { id: 'task-2', label: 'Invoice draft', description: 'Backlog' }
        ]

        const resolved = resolveOne({ query: 'invoice' }, matches, {
          nextStep: 'Retry with an explicit id'
        })

        if (!resolved.ok) return resolved
        return ok({ type: 'task', id: resolved.candidate.id, record: resolved.candidate })
      }
    })
  ]
})

Recovery, mutation safety, and risk

AgentTK now exposes the framework-level execution metadata that an autonomous agent actually needs when things get messy.

  • withRecovery(...) adds nextAction, classification, and retryable
  • withMutationSafety(...) adds retrySafety, replayRisk, partial, verified, verificationStatus, and idempotencyKey
  • defineRisk(...) and requireConfirmation(...) let downstream tools declare and enforce risk posture without hard-coding one approval UX
import {
  defineRisk,
  markUnverified,
  ok,
  requireConfirmation,
  withMutationSafety,
  withRecovery
} from 'agenttk'

const destructiveRisk = defineRisk({
  level: 'destructive',
  confirmation: 'required',
  reason: 'Deletes all matching records in scope'
})

const blocked = requireConfirmation(false, destructiveRisk, {
  nextStep: 'Re-run with --confirm if you really mean it'
})

const created = withMutationSafety(
  withRecovery(
    ok({ type: 'card', id: 'card-123', record: { title: 'Pilot card' } }),
    { nextAction: 'continue', classification: 'unknown', retryable: false }
  ),
  {
    retrySafety: 'verify_first',
    replayRisk: 'high',
    verificationStatus: 'unverified'
  }
)

const pendingVerification = markUnverified(created)

if (blocked !== true) {
  console.log(blocked.error.code)
}

Agent-safe CLI checklist

AgentTK treats “agent-safe” as a review bar, not marketing fluff. The exported checklist covers the minimum posture a downstream CLI should meet before autonomous use:

  • predictable failure envelopes
  • machine-usable recovery semantics
  • explicit retry and replay posture for writes
  • post-mutation verification
  • risk and confirmation posture for dangerous commands
  • review-grade test coverage
import { agentSafeCliChecklist } from 'agenttk'

for (const item of agentSafeCliChecklist) {
  console.log(item.id, item.title)
}

Use it in PR reviews, CLI audits, or release gates for downstream tools.

Testing helpers

Use the richer assertions when you want cheap confidence around framework primitives, not shell-heavy rechecks of every field by hand.

import test from 'node:test'
import { authFailureFixture, expectAuthFailure, requireAuth } from 'agenttk'

test('auth failure stays structured', async () => {
  const auth = await requireAuth(authFailureFixture({
    code: 'ACCOUNT_MISMATCH',
    currentAccount: 'personal@example.com',
    expectedAccount: 'team@example.com'
  }))

  expectAuthFailure(auth, {
    code: 'ACCOUNT_MISMATCH',
    provider: 'google',
    currentAccount: 'personal@example.com',
    expectedAccount: 'team@example.com'
  })
})

Keep using plain runTool(...) tests for command wiring and output mode checks. Use the richer helpers when you want fast, data-first checks around auth, lookup, dry-run, adapter, and config behavior.

Examples

  • examples/minimal-cli/ - basic runtime, help, and JSON output
  • examples/tasks-cli/ - validation, dry-run, and command-style mutations
  • examples/adapter-cli/ - capability checks and normalized adapter failures
  • examples/config-cli/ - profile selection and config diagnostics
  • examples/testing-fixtures/ - richer framework-aware test assertions
node examples/minimal-cli/index.mjs hello --json
node examples/minimal-cli/index.mjs help
node examples/tasks-cli/index.mjs add --title "Send estimate" --dry-run --json
node examples/tasks-cli/index.mjs add
node examples/adapter-cli/index.mjs status --json
node examples/config-cli/index.mjs show --profile work

Development

npm run build
npm test
npm run examples:smoke
npm run verify

Changelog

See CHANGELOG.md.

Release discipline

For normal releases:

npm version patch
git push origin main --follow-tags

Rules:

  • release only from a clean main
  • let CI verify before trusting the tag
  • keep package.json, npm, and GitHub releases aligned
  • document user-visible changes in CHANGELOG.md

About

No description, website, or topics provided.

Resources

License

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors