feat: initial commit
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@agent-studio/shared",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": { ".": { "types": "./src/index.ts", "development": "./src/index.ts", "import": "./dist/index.js", "default": "./dist/index.js" } },
|
||||
"scripts": { "typecheck": "tsc --noEmit", "build": "tsc -p tsconfig.json" },
|
||||
"dependencies": { "zod": "^4.1.13" },
|
||||
"devDependencies": { "typescript": "^5.9.3" }
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { z } from "zod";
|
||||
import { approvalRequestSchema, imageAttachmentSchema, llmApiSchema } from "./schemas.js";
|
||||
|
||||
const baseShape = {
|
||||
eventId: z.string(),
|
||||
sessionId: z.string(),
|
||||
runId: z.string().optional(),
|
||||
timestamp: z.string().datetime(),
|
||||
sequence: z.number().int().positive(),
|
||||
};
|
||||
|
||||
const event = <T extends string, S extends z.ZodType>(type: T, payload: S) =>
|
||||
z.object({ ...baseShape, type: z.literal(type), payload }).strict();
|
||||
|
||||
const empty = z.object({}).strict();
|
||||
const errorPayload = z.object({ code: z.string(), message: z.string() }).strict();
|
||||
const toolBase = z.object({ toolCallId: z.string(), toolName: z.string(), arguments: z.unknown().optional() }).strict();
|
||||
|
||||
export const harnessEventSchema = z.discriminatedUnion("type", [
|
||||
event("session.started", z.object({ cwd: z.string() }).strict()),
|
||||
event("session.ended", z.object({ reason: z.enum(["deleted", "shutdown", "error"]) }).strict()),
|
||||
event("user.message", z.object({ messageId: z.string(), text: z.string(), attachmentIds: z.array(z.string()) }).strict()),
|
||||
event("assistant.delta", z.object({ messageId: z.string(), delta: z.string() }).strict()),
|
||||
event("assistant.completed", z.object({ messageId: z.string() }).strict()),
|
||||
event("skill.requested", z.object({ name: z.string(), filePath: z.string(), toolCallId: z.string() }).strict()),
|
||||
event("skill.loaded", z.object({ name: z.string(), filePath: z.string(), toolCallId: z.string() }).strict()),
|
||||
event("tool.requested", toolBase),
|
||||
event("tool.completed", z.object({ toolCallId: z.string(), toolName: z.string(), result: z.unknown().optional() }).strict()),
|
||||
event("tool.failed", z.object({ toolCallId: z.string(), toolName: z.string(), error: z.string() }).strict()),
|
||||
event("approval.required", approvalRequestSchema),
|
||||
event("approval.resolved", z.object({ approvalId: z.string(), toolCallId: z.string(), status: z.enum(["approved", "denied", "expired", "cancelled"]), resolvedAt: z.string().datetime() }).strict()),
|
||||
event("attachment.uploaded", imageAttachmentSchema.omit({ sessionId: true })),
|
||||
event("attachment.rejected", z.object({ name: z.string(), code: z.string(), message: z.string() }).strict()),
|
||||
event("model.configured", z.object({ providerId: z.string(), modelId: z.string(), api: llmApiSchema, supportsImages: z.boolean() }).strict()),
|
||||
event("model.validation_failed", errorPayload),
|
||||
event("run.started", z.object({ modelId: z.string() }).strict()),
|
||||
event("run.cancelled", z.object({ reason: z.enum(["user", "timeout", "session_deleted", "shutdown"]) }).strict()),
|
||||
event("run.completed", empty),
|
||||
event("run.failed", errorPayload),
|
||||
]);
|
||||
|
||||
export type HarnessEvent = z.infer<typeof harnessEventSchema>;
|
||||
export type HarnessEventType = HarnessEvent["type"];
|
||||
export type EventOfType<T extends HarnessEventType> = Extract<HarnessEvent, { type: T }>;
|
||||
export type EventInput<T extends HarnessEventType> = Omit<EventOfType<T>, "eventId" | "sessionId" | "timestamp" | "sequence">;
|
||||
|
||||
export const sseControlEventSchema = z.object({
|
||||
type: z.literal("stream.gap"),
|
||||
oldestAvailableSequence: z.number().int().positive(),
|
||||
requestedSequence: z.number().int().nonnegative(),
|
||||
}).strict();
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./schemas.js";
|
||||
export * from "./events.js";
|
||||
@@ -0,0 +1,91 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const llmApiSchema = z.enum(["openai-responses", "openai-completions", "anthropic-messages"]);
|
||||
|
||||
export const modelConnectionInputSchema = z.object({
|
||||
providerId: z.string().trim().min(1).max(80).regex(/^[a-zA-Z0-9._-]+$/),
|
||||
api: llmApiSchema,
|
||||
baseUrl: z.string().trim().url().max(2048).optional(),
|
||||
apiKey: z.string().min(1).max(8192).optional(),
|
||||
modelId: z.string().trim().min(1).max(200),
|
||||
displayName: z.string().trim().min(1).max(200).optional(),
|
||||
supportsImages: z.boolean(),
|
||||
contextWindow: z.number().int().min(1_024).max(10_000_000).optional(),
|
||||
maxTokens: z.number().int().min(1).max(1_000_000).optional(),
|
||||
}).strict();
|
||||
export type ModelConnectionInput = z.infer<typeof modelConnectionInputSchema>;
|
||||
|
||||
export const persistedModelConnectionSchema = modelConnectionInputSchema.omit({ apiKey: true });
|
||||
export type PersistedModelConnection = z.infer<typeof persistedModelConnectionSchema>;
|
||||
|
||||
export const redactedModelConnectionSchema = z.object({
|
||||
configured: z.boolean(),
|
||||
source: z.enum(["session", "environment", "missing"]),
|
||||
providerId: z.string().optional(),
|
||||
api: llmApiSchema.optional(),
|
||||
baseUrl: z.string().optional(),
|
||||
modelId: z.string().optional(),
|
||||
displayName: z.string().optional(),
|
||||
supportsImages: z.boolean().optional(),
|
||||
keyConfigured: z.boolean(),
|
||||
keyHint: z.string().optional(),
|
||||
credentialStorage: z.enum(["memory", "encrypted", "environment"]).optional(),
|
||||
}).strict();
|
||||
export type RedactedModelConnection = z.infer<typeof redactedModelConnectionSchema>;
|
||||
|
||||
export const sendMessageSchema = z.object({
|
||||
text: z.string().max(100_000).default(""),
|
||||
attachmentIds: z.array(z.string().regex(/^att_[a-zA-Z0-9_-]+$/)).max(16).default([]),
|
||||
}).strict().refine((value) => value.text.trim().length > 0 || value.attachmentIds.length > 0, {
|
||||
message: "Text or at least one attachment is required",
|
||||
});
|
||||
export type SendMessageInput = z.infer<typeof sendMessageSchema>;
|
||||
|
||||
export const imageAttachmentSchema = z.object({
|
||||
attachmentId: z.string(),
|
||||
sessionId: z.string(),
|
||||
name: z.string(),
|
||||
mediaType: z.enum(["image/jpeg", "image/png", "image/webp"]),
|
||||
byteSize: z.number().int().nonnegative(),
|
||||
width: z.number().int().positive(),
|
||||
height: z.number().int().positive(),
|
||||
status: z.literal("ready"),
|
||||
}).strict();
|
||||
export type ImageAttachment = z.infer<typeof imageAttachmentSchema>;
|
||||
|
||||
export const approvalRequestSchema = z.object({
|
||||
approvalId: z.string(),
|
||||
sessionId: z.string(),
|
||||
runId: z.string(),
|
||||
toolCallId: z.string(),
|
||||
toolName: z.string(),
|
||||
arguments: z.unknown(),
|
||||
riskLevel: z.enum(["low", "medium", "high"]),
|
||||
createdAt: z.string().datetime(),
|
||||
expiresAt: z.string().datetime(),
|
||||
status: z.enum(["pending", "approved", "denied", "expired", "cancelled"]),
|
||||
}).strict();
|
||||
export type ApprovalRequest = z.infer<typeof approvalRequestSchema>;
|
||||
|
||||
export const apiErrorSchema = z.object({
|
||||
error: z.object({
|
||||
code: z.string(),
|
||||
message: z.string(),
|
||||
requestId: z.string(),
|
||||
details: z.record(z.string(), z.unknown()),
|
||||
}).strict(),
|
||||
}).strict();
|
||||
export type ApiError = z.infer<typeof apiErrorSchema>;
|
||||
|
||||
export const skillDiagnosticSchema = z.object({
|
||||
severity: z.enum(["warning", "error"]),
|
||||
message: z.string(),
|
||||
}).strict();
|
||||
export const skillCatalogItemSchema = z.object({
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
source: z.string(),
|
||||
filePath: z.string(),
|
||||
diagnostics: z.array(skillDiagnosticSchema),
|
||||
}).strict();
|
||||
export type SkillCatalogItem = z.infer<typeof skillCatalogItemSchema>;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { apiErrorSchema, harnessEventSchema, modelConnectionInputSchema, persistedModelConnectionSchema, sendMessageSchema } from "../src/index.js";
|
||||
|
||||
describe("shared boundary schemas", () => {
|
||||
it("accepts a typed HarnessEvent variant", () => {
|
||||
expect(harnessEventSchema.parse({ eventId: "evt_1", sessionId: "ses_1", runId: "run_1", timestamp: new Date().toISOString(), sequence: 1, type: "assistant.delta", payload: { messageId: "msg_1", delta: "Hi" } }).type).toBe("assistant.delta");
|
||||
});
|
||||
it("rejects the wrong discriminated payload", () => {
|
||||
expect(() => harnessEventSchema.parse({ eventId: "e", sessionId: "s", timestamp: new Date().toISOString(), sequence: 1, type: "run.completed", payload: { unexpected: true } })).toThrow();
|
||||
});
|
||||
it("strictly validates model configuration", () => {
|
||||
expect(() => modelConnectionInputSchema.parse({ providerId: "openai", api: "made-up", modelId: "x", supportsImages: false })).toThrow();
|
||||
expect(() => modelConnectionInputSchema.parse({ providerId: "openai", api: "openai-responses", modelId: "x", supportsImages: false, headers: {} })).toThrow();
|
||||
});
|
||||
it("accepts valid model configuration", () => {
|
||||
expect(modelConnectionInputSchema.parse({ providerId: "openai", api: "openai-responses", modelId: "gpt", supportsImages: true }).modelId).toBe("gpt");
|
||||
});
|
||||
it("persists only non-sensitive model configuration", () => {
|
||||
const value = { providerId: "openai", api: "openai-responses", modelId: "gpt", supportsImages: true };
|
||||
expect(persistedModelConnectionSchema.parse(value)).toEqual(value);
|
||||
expect(() => persistedModelConnectionSchema.parse({ ...value, apiKey: "secret" })).toThrow();
|
||||
});
|
||||
it("requires text or attachments", () => {
|
||||
expect(() => sendMessageSchema.parse({ text: "", attachmentIds: [] })).toThrow();
|
||||
expect(sendMessageSchema.parse({ text: "", attachmentIds: ["att_ok"] }).attachmentIds).toEqual(["att_ok"]);
|
||||
});
|
||||
it("rejects malformed attachment ids", () => {
|
||||
expect(() => sendMessageSchema.parse({ text: "x", attachmentIds: ["../escape"] })).toThrow();
|
||||
});
|
||||
it("validates the unified error envelope", () => {
|
||||
expect(apiErrorSchema.parse({ error: { code: "SESSION_NOT_FOUND", message: "Session not found", requestId: "req_1", details: {} } }).error.code).toBe("SESSION_NOT_FOUND");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": { "rootDir": "src", "outDir": "dist", "tsBuildInfoFile": "dist/.tsbuildinfo" },
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user