Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions .claude/skills/create-agent-pack/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,22 @@ there is no separate "save" step. Proceed when upload succeeds and reports the n

```bash
npx packs validate pack.ts # single pre-upload check: schema, instructions, triggers, connector warnings
npx packs agent install pack.ts # binds the latest upload and saves the instance id
npx packs agent install pack.ts # binds the latest upload
npx packs agent chat pack.ts "what can you do?" # remote run
```

Run `validate` after any edit, before uploading. For agent packs it also warns about
connector pack IDs that don't resolve (when you're logged in; pass `--no-checkConnectors`
to skip). Run `agent install` after uploading. It prints the agent instance id and tenant
id and saves them in `.coda-agent.json` for this directory. Run `agent chat` after that.
It uses the saved instance and polls until the reply lands (or `--timeout` expires).
Pass `--agentInstanceId` or `--tenantId` only to override. The command checks the instance
belongs to this pack. Multi-turn threads are not supported yet: every chat starts a new run.
id. Run `agent chat` after that. Chat looks up the install for this pack and polls until
the reply lands (or `--timeout` expires). Pass `--agentInstanceId` or `--tenantId` only to
override. The command checks the instance belongs to this pack.

Consecutive prompts share one thread. Chat saves the chat id in `~/.coda-agent-chats.json`
(per user, per pack directory, never committed) and sends it on the next prompt, so a follow-up sees earlier turns. Chat prints which chat it continues and when it was last used. A chat idle for more than a day is not continued by default: chat starts a new one and prints the old id. Pass `--new` to
start a fresh thread, or `--chatId <id>` to continue a specific chat (its agent runs unless you
also pass `--agentInstanceId`). A chat id that is missing, not yours, or on another agent fails
with "That chat is gone"; run again with `--new`.

### 6. Set the listing name/description (UI)

Expand Down
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ This changelog keeps track of all changes to the Packs SDK. We follow convention

## [Unreleased]

### Added

- `packs agent chat` now continues the previous thread, so consecutive prompts share one chat. It saves the chat id per user in `~/.coda-agent-chats.json`, so nothing is added to your repo. Pass `--new` to start a fresh thread, or `--chatId` to continue a specific chat. A chat that no longer exists fails with a message telling you to rerun with `--new`. Requires a server that accepts `chatId`; an older server starts a new chat each time.
- `packs agent chat` now says which chat it is continuing and when it was last used. A saved chat idle for more than 24 hours is no longer continued by default. Chat starts a new one and prints `--chatId <id>` to go back to the old one.

## [1.18.3] - 2026-10-15

### Added

- `packs agent install` prints the agent instance id and tenant id, and saves them in `.coda-agent.json` for that directory. The next `packs agent chat` and `packs agent logs` in the directory use the saved ids, so you no longer copy them out of the builder URL. Pass `--agentInstanceId`, `--tenantId`, `--tenant`, or `--instance` to override. New `packs init` scaffolds ignore `.coda-agent.json`. Add that filename to `.gitignore` in directories created before this change.
- `packs agent chat` and `packs agent logs` look up the caller's install for this pack, including the tenant id. Pass `--agentInstanceId`, `--tenantId`, `--tenant`, or `--instance` only to override. `packs agent install` still prints those ids and no longer writes them to a file. `packs agent install --version` selects the uploaded pack version.
- `addSuggestedPrompt()` on agents, so an agent can declare up to 3 starter prompts that show as buttons on its chat landing. Agents no longer fail validation when they define `suggestedPrompts`.

## [1.18.2] - 2026-10-14
Expand Down
15 changes: 15 additions & 0 deletions api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1112,12 +1112,27 @@ type V2PackFormula<ParamDefsT extends ParamDefs, SchemaT extends Schema = Schema
export type TypedPackFormula = Formula | GenericSyncFormula;

export type TypedObjectPackFormula = ObjectPackFormula<ParamDefs, Schema>;

/**
* UI configuration carried in top-level formula metadata.
* @hidden
*/
export interface PackFormulaUi {
/** References a resource descriptor's key. */
resourceKey: string;
}

/** @hidden */
export type PackFormulaMetadata = Omit<
TypedPackFormula,
'execute' | 'executeUpdate' | 'executeGetPermissions' | 'validateParameters'
> & {
validateParameters?: MetadataFormulaMetadata<ExecutionContext, ParameterValidationResult>;
/**
* UI configuration for this top-level formula.
* @hidden
*/
ui?: PackFormulaUi;
};
/** @hidden */
export type ObjectPackFormulaMetadata = Omit<TypedObjectPackFormula, 'execute' | 'validateParameters'> & {
Expand Down
39 changes: 37 additions & 2 deletions builder.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {AdminAuthentication} from './types';
import type {AdminAuthenticationDef} from './types';
import type {AgentDefinition} from './types';
import type {AgentRoutineDefinition} from './types';
import type {AgentTool} from './types';
import type {AgentToolsDef} from './types';
import type {AllowedAuthentication} from './types';
Expand Down Expand Up @@ -67,7 +68,7 @@ import {wrapMetadataFunction} from './api';
* ```
*/
export function newPack(
definition?: Partial<Omit<PackVersionDefinition, 'agent' | 'defaultTriggers'>>,
definition?: Partial<Omit<PackVersionDefinition, 'agent' | 'defaultTriggers' | 'defaultRoutines'>>,
): PackDefinitionBuilder {
return new PackDefinitionBuilder(definition);
}
Expand Down Expand Up @@ -196,7 +197,7 @@ export class PackDefinitionBuilder extends BaseDefinitionBuilder implements Basi
* Constructs a {@link PackDefinitionBuilder}. However, `sdk.newPack()` should be used instead
* rather than constructing a builder directly.
*/
constructor(definition?: Partial<Omit<PackVersionDefinition, 'agent' | 'defaultTriggers'>>) {
constructor(definition?: Partial<Omit<PackVersionDefinition, 'agent' | 'defaultTriggers' | 'defaultRoutines'>>) {
super();
const {
formulas,
Expand Down Expand Up @@ -669,6 +670,7 @@ interface AgentBuilderState {
agent: Partial<AgentDefinition>;
defaultTriggers?: DefaultTriggerDefinition[];
suggestedPrompts?: SuggestedPrompt[];
defaultRoutines?: AgentRoutineDefinition[];
}

// Off the instance: an own property leaks into the metadata, and `#private` breaks pre-ES2015 .d.ts consumers.
Expand Down Expand Up @@ -699,6 +701,8 @@ export class AgentDefinitionBuilder extends BaseDefinitionBuilder {
* See {@link PackVersionDefinition.suggestedPrompts}. Set via {@link addSuggestedPrompt}.
*/
declare readonly suggestedPrompts?: DeepReadonly<SuggestedPrompt[]>;
/** See {@link PackVersionDefinition.defaultRoutines}. Set via {@link addDefaultRoutine}. */
declare readonly defaultRoutines?: DeepReadonly<AgentRoutineDefinition[]>;

constructor() {
super();
Expand All @@ -708,6 +712,37 @@ export class AgentDefinitionBuilder extends BaseDefinitionBuilder {
Object.defineProperty(this, 'agent', {enumerable: true, get: () => state.agent});
Object.defineProperty(this, 'defaultTriggers', {enumerable: true, get: () => state.defaultTriggers});
Object.defineProperty(this, 'suggestedPrompts', {enumerable: true, get: () => state.suggestedPrompts});
Object.defineProperty(this, 'defaultRoutines', {enumerable: true, get: () => state.defaultRoutines});
}

/**
* Adds a routine with its own name or instructions and one trigger.
*
* @example
* ```
* pack.addDefaultRoutine({
* name: 'Daily brief',
* instructions: 'Summarize overnight changes. Include source links.',
* trigger: {
* kind: sdk.DefaultTriggerKind.Schedule,
* rruleString: 'DTSTART;TZID=America/New_York:20261012T090000\nRRULE:FREQ=DAILY',
* },
* });
* ```
*/
addDefaultRoutine(routine: AgentRoutineDefinition): this {
if (!routine.name?.trim() && !routine.instructions?.trim()) {
throw new Error('A routine needs a name or instructions containing non-whitespace text.');
}
if (routine.trigger.kind === DefaultTriggerKind.Schedule) {
const message = validateRRuleString(routine.trigger.rruleString);
if (message) {
throw new Error(message);
}
}
const state = stateOf(this);
state.defaultRoutines = frozenCopy<AgentRoutineDefinition[]>([...(state.defaultRoutines ?? []), routine]);
return this;
}

/**
Expand Down
109 changes: 109 additions & 0 deletions cli/agent_api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,112 @@ export async function postAgentRoute(route: string, args: PostAgentRouteArgs, bo
}
return response;
}

export interface InstalledAgent {
agentInstanceId: string;
tenantId: string;
}

export type InstalledAgentLookup =
| {status: 'found'; agent: InstalledAgent}
| {status: 'missing'}
| {status: 'unavailable'}
| {status: 'error'; message: string};

export function buildInstalledAgentUrl(apiEndpoint: string, packId: number): string {
return `${formatEndpoint(apiEndpoint)}/apis/v1/packs/${packId}/agentInstance`;
}

export function codaTypeOf(bodyText: string): string | undefined {
let parsed: unknown;
try {
parsed = JSON.parse(bodyText);
} catch {
return undefined;
}
if (typeof parsed !== 'object' || parsed === null) {
return undefined;
}
const codaType = (parsed as {codaType?: unknown}).codaType;
return typeof codaType === 'string' ? codaType : undefined;
}

function lookupError(message: string): InstalledAgentLookup {
return {status: 'error', message};
}

export async function lookupInstalledAgent(args: {
apiToken: string;
apiEndpoint: string;
packId: number;
}): Promise<InstalledAgentLookup> {
const url = buildInstalledAgentUrl(args.apiEndpoint, args.packId);
let response: Response;
try {
response = await fetch(url, {
method: 'GET',
headers: {Authorization: `Bearer ${args.apiToken}`},
signal: AbortSignal.timeout(30000),
});
} catch (err: any) {
const errors = [`Error while looking up the installed agent: ${err}`, tryParseSystemError(err)];
return lookupError(errors.join('\n'));
}

const bodyText = await response.text();
if (response.status === 404 && !isApiErrorBody(bodyText)) {
return {status: 'unavailable'};
}
if (response.status === 404 && codaTypeOf(bodyText) === 'AgentInstanceNotFound') {
return {status: 'missing'};
}
if (response.status === 404) {
return lookupError('Could not look up the installed agent.');
}
if (!response.ok) {
return lookupError(`Could not look up the installed agent: ${response.status} ${response.statusText}`);
}

let parsed: unknown;
try {
parsed = JSON.parse(bodyText);
} catch (err: any) {
return lookupError(`Installed agent lookup failed: the server response was not JSON. ${err}`);
}
const record = parsed as {agentInstanceId?: unknown; tenantId?: unknown};
if (typeof record.agentInstanceId !== 'string' || record.agentInstanceId.trim() === '') {
return lookupError('Installed agent lookup failed: the server response did not include an agent instance id.');
}
if (typeof record.tenantId !== 'string' || record.tenantId.trim() === '') {
return lookupError('Installed agent lookup failed: the server response did not include a tenant id.');
}
return {
status: 'found',
agent: {agentInstanceId: record.agentInstanceId, tenantId: record.tenantId},
};
}

export function installedAgentLookupError(
lookup: Exclude<InstalledAgentLookup, {status: 'found'}>,
kind: 'chat' | 'logs',
): string {
if (lookup.status === 'error') {
return lookup.message;
}
if (lookup.status === 'unavailable') {
const override = kind === 'chat' ? '--agentInstanceId' : '--tenant and --instance';
return `The installed-agent lookup is not available on this server yet.\n Pass ${override}.`;
}
if (kind === 'chat') {
return (
'Error: agent chat needs an installed agent instance.\n' +
' packs agent install pack.ts\n' +
' Or pass --agentInstanceId.'
);
}
return (
'Error: agent logs need a tenant and an agent instance.\n' +
' packs agent install pack.ts\n' +
' Or pass --tenant and --instance.'
);
}
Loading
Loading