cd /news/ai-agents/show-hn-ctx-traits-agent-traits-and-… · home topics ai-agents article
[ARTICLE · art-78686] src=ctx.company ↗ pub= topic=ai-agents verified=true sentiment=· neutral

Show HN: Ctx.traits · Agent Traits and Workflows typed, versioned and in-sync

Ctx.traits, a new open-source framework for defining agent behaviors and workflows as typed, versioned modules, has been released on GitHub. The framework allows developers to express agent traits declaratively in code rather than prose prompts, aiming to improve maintainability and testability across teams. Creator Alex K. Chen says the approach balances abstractions with interfaces to make agent reasoning more predictable.

read20 min views1 publishedJul 29, 2026

Express your team's agent behaviors and workflows as typed, versioned modules. Review and compose them directly in code, instead of copying prompts by hand.

Like many working in this early era of AI-driven systems, I have struggled to find an approach to agents that feels satisfying. As models improve at problem-solving, the tools and approaches often feel like either a blind bet or an afterthought.

Each new trend or pattern feels like another brute-force attempt to bargain with the model, hoping to make it predictable by force. These approaches tend to come with a long list of requirements and an implicit agreement to ignore their shortcomings, leaving me with the ongoing chore of babysitting and double-checking at multiple levels.

The field has cycled through prompt engineering, context engineering, loop engineering and now graph engineering. Each comes with its own quirks and assumptions. The challenge is taking what works from each, without pretending productivity is improving when it is not.

I am not claiming to have found the right approach, but the process has been gratifying, and the result feels like a step in the right direction for me.

In the end, it came down to the same thing that has proven itself throughout my career: a careful balance between abstractions and the right interfaces, combined with the model’s ability to solve problems using the available tools.

Prompt · Contract over prose #

Here’s where things broke down fast: from the moment I started working with agents, stuffing everything into Markdown files full of prose was a trap. I kept trying to add structure, but it always turned into a constant slog of fighting the model, and an abstraction that never felt sturdy enough to rely on.

It quickly became clear that putting too much logic into a text file and expecting an AI agent to interpret it as I would was not a good idea. Models struggled to follow all the instructions in a file and often tried to resolve contradictions on their own. Tracking organization and optimization through these crafted text files became a burden to test and maintain, especially across a team.

This led to the first key decision: to construct instructions that are maintainable, testable, and declarative. They should be easy for agents to understand and for humans to reason about. My first attempt was a configuration-file approach, which at first felt similar to working with Terraform. It quickly proved too verbose and not enjoyable to use.

This led me to implement a CDK-like solution that still targets a static configuration file as a synthesized contract. Defining the logic as code felt like the only sane path forward.

export default trait("deep-refactor", {
    version: "1.0.0",
    name: "Deep Refactor",
    summary: "Refactor the codebase to a more structured and maintainable state.",
    metadata: {
        tag: ["first-party", "refactoring", "review", "multi-agent"],
    },
    intent: {
        require: [intent.BoundedRefinement, intent.PreserveOriginalBehavior],
        avoid: [intent.InterfaceWidening, intent.UnboundedLoop],
        block: [intent.StyleOnlyChange],
    },
    behavior: {
        tone: [tone.Technical, tone.Direct],
        verbosity: verbosity.Low,
        method: method.EvidenceFirst,
    },
});

This declarative configuration makes the trait’s intent and expected behavior clear to agents. For humans, a quick look at the code should be enough to understand how the trait works and, more importantly, to maintain it with confidence. The aim is to make reasoning about the system straightforward.

Defining traits this way made it possible to automate more of the development process. Desired behaviors and instructions became extendable and composable, enabling the reuse of approaches across projects and avoiding copy-paste or prompt hacks.

Context · Art of reveal and conceal #

The next problem on my list was the one I had the most experience working with and trying different approaches to. Approaches discovered so far all sounded like great ideas on paper, formed around how individuals approach working with problems, but all lacking in either ergonomics or the confidence with which their outcomes can be trusted:

Swarm agents- where you state the problem and delegate the work to a group of agents, aiming to reduce multiple opinions into one broad-view solutionPer-task compaction- where you work on a single problem within a single window of context, compacting afterward, hoping to “regain” the model’s intelligence, while keeping the high-level context of the projectAutomatic compaction- where you’re constantly working with a seemingly “infinite” window of context, hoping that the agent is smart enough to compress the knowledge for eternity and make it “permanent”Handoff- where you’re working with a single agent (e.g., planner) and hand over its definition of the problem to a different agent (e.g., worker) to actually implement it

After spending considerable time with these approaches, I have concluded that careful handling of context is essential. This often lets me work around imperfect abstractions and lets the models do more of the heavy lifting in finding the right solution.

Still, I often felt the context was not being used to its full potential, which seemed like a missed opportunity. The next realization was that not all context is equal. What you put in determines what you get out.

There is a subtle art to deciding what to reveal, what to conceal, and when. Not all agents need to know everything, and how they share knowledge with each other must be controlled. This is as much about discipline as about tooling.

Input / Output for the agent

One of the things that you might want to keep in context is the input and output of the change you’re trying to make. For that, you need to declare port.input or port.output in your trait. These are abstractions that enable your trait to receive and return data.

const changeRequest = port.input.text("change-request", {
    description: "The change to make — a bug to fix or a feature to add.",
});

const changeTarget = port.input.text("change-target", {
    description: "Where the change applies — the repository path, module, or subject.",
});

const commitReport = port.output.text("commit-report", {
    description: "Final commit evidence from the `git commit` command step.",
});

Storage for the agent

Another thing that you might want to keep in context is the result of specific parts of the work that one or more agents are working on. For that, you need to declare a slot in your trait. These abstractions allow your trait to store data that can be reused in different places during the task’s runtime.

const workSummary = slot.text("work-summary", {
    description: "Worker's report of the state of the work implemented.",
    hint: "What work has been done, what was the approach and potential concerns.",
});

const reviewVerdict = slot.verdict("review-verdict", {
    description: "Reviewer's verdict on the work implemented.",
    hint: "Has the reviewer approved the work, or does it still need some changes?",
});

const commitReport = port.output.text("commit-report", {
    description: "Scribe's final commit, including title & content.",
    hint: "What was implemented, grounded in initial request and worker's summary.",
});

Storing data is only part of the problem. Enforcing its correct shape is just as important. At runtime, regardless of how the trait is used, both port and slot require data to follow a strictly typed schema. There are helpers for common types like slot.text, but you can define your own schemas as needed.

const verdict = schema.object("verdict", {
    status: schema.enum(["approved", "rejected", "needs-changes"]),
    blockers: schema.texts({ required: false }),
    advisory: schema.texts({ required: false }),
});

Resources for the agent

Another key consideration is which resources are available to the agent. This is often handled by mentioning files, directories, or artifacts directly in the prompt or skill, but precise control over which resources are available to which agents and how is crucial. Without this, the model can easily become confused about what it should know and what to base its decisions on.

This leads to the concept of resources in trait definitions. Resources are abstractions for passing data to agents, giving precise control over when and how data is provided.

const reviewGuidelines = resource("review-guidelines", {
    path: "resources/review-guidelines.md",
    hint: "Review guidelines that reviewers score implementation against.",
    render: resource.render.Inline,
});

const codeStandards = resource("code-standards", {
    path: "resources/code-standards.md",
    hint: "Code standards that might be useful to workers.",
    render: resource.render.Reference,
    trigger: resource.trigger.OnDemand,
});

This lets me control whether resources are inlined in context or only referenced for retrieval as needed. I can also specify whether a resource is available immediately or only upon explicit agent request, using resource discovery tools. This level of control helps avoid accidental leakage of information.

Rot-Protection

Finally, it is important to prevent traits from being used in ways that overload the context with unnecessary data. This is less about specific abstractions and more about how they are used together to control and optimize context.

In this example, slot and resource are used to ground the agent in the context of the project, its conventions, and the current task. The agent is also instructed to plan the work so that subsequent agents do not need to reread the same plan each time.

const featureRequest = port.input.text("feature-request", {
    description: "Feature to implement in the project.",
});

const productSpec = resource("product-spec", {
    path: "@docs/product.md", // Path relative to the project root.
    hint: "Specification of the project being worked on.",
});

const implementationPlan = slot.text("implementation-plan", {
    description: "Plan of the work to be done.",
    hint: "Detailed plan for the implementation of the feature.",
});

..

sequence.prompt("Implementation Planning", {
    title: "Build the implementation plan",
    text: "Create a detailed implementation plan for the feature.",
    agent: planningAgent,
    input: [featureRequest, productSpec],
    output: implementationPlan,
})

..

sequence.prompt("Implementation", {
    title: "Work on the implementation",
    text: "Implement the feature.",
    agent: workerAgent,
    input: implementationPlan,
})

..

This is a straightforward example of the mechanism described above: the request and project specification are loaded once and compiled into an implementation plan. When it is time to implement, the worker agent receives the detailed plan to follow.

With this, the cluttered context approach was replaced with a deterministic way to wire data flow between agents and resources. This set the stage for me to focus on how agents actually perform work in a controlled way.

Loop · Comfort of being in control #

The last part of this initial exploration was the loop. I postponed tackling it for a while, assuming the loop approach was already solid and solved. That assumption did not hold up.

I revisited this problem after spending more time with newer models. Constant delegation to sub-agents felt excessive, and I found myself less in control of what was actually happening in the process.

I was skeptical of the promised benefits of work parallelization and combining multi-angle results into a single solution. Even after a few successful attempts, I found the approach unsatisfying.

After some exploration, I realized that to truly harness the workflow, the runtime itself needs to own the outer loop where agents cooperate. This led to the need for an explicit set of control features:

Declaration of sequence order

The sequence should be declared in a clear, easily understood way. It should read like a checklist, ready to be iterated over and executed by agents. This makes the process easier to reason about and maintain.

procedure([
    sequence.prompt("step-a", { text: "Execute Step A" }),
    sequence.prompt("step-b", { text: "Execute Step B" }),
])

Declaration of agent assignment to sequence

Each step should be executed by either a default agent or explicitly assigned to a specialized agent. Different steps require different reasoning and expertise. Assigning agents deliberately helps avoid accidental mismatches.

procedure([
    sequence.prompt("step-a", { agent: workerAgent }),
    sequence.prompt("step-b", { agent: reviewAgent }),
])

Declaration of relationship between sequence’s steps

It must be clear how each step relates to the others: which steps inherit results, what outputs are produced, and who uses them. This also lets me confidently specify which steps can run in parallel. Without this, the process becomes difficult to debug.

procedure([
    sequence.parallel([
        sequence.prompt("step-a", { text: "Execute Step A" }, output: slotA),
        sequence.prompt("step-b", { text: "Execute Step B" }, output: slotB),
    ]),
    sequence.prompt("step-c", { text: "Execute Step C" }, input: [slotA, slotB] }),
])

Declaration of data wiring between steps

It is crucial to define precisely which data each step uses. This ensures that each step receives exactly what it needs, no more and no less.

procedure([
    sequence.prompt("step-a", { text: "Execute Step A" }, input: resourceA, output: slotA),
    sequence.prompt("step-b", { text: "Execute Step B" }, input: resourceB, output: slotB),
    sequence.prompt("step-c", { text: "Execute Step C" }, input: [slotA, slotB],
])

Deterministic process declared as a step

This lets me declare a step as a specific CLI command, which does not need to be agent-initiated. I control when the command’s output enters the step’s context, avoiding unnecessary data in the agent’s context. This helps keep the process predictable.

procedure([
    sequence.command("git-stage",  { text: "git add -A" }),
    sequence.command("git-commit", { text: command.text`git commit "${commitMessage}"` }),
])

Declaration of loop iteration exit / satisfaction

It is important to state explicitly when a loop iteration is complete and the process can continue. This lets me fine-tune the work and review process, ensuring the agent continues until the result is satisfactory. Without this, loops can easily become open-ended.

procedure([
    sequence.loop("step-loop", [
        sequence.prompt("step-a", { text: "Execute Step A" }, input: resourceA, output: slotA),
        sequence.prompt("step-b", { text: "Execute Step B" }, input: resourceB, output: slotB),
    ], {
        until: condition.all([
            condition.equals(slotA, schema.bool.True),
            condition.equals(slotB, schema.bool.True),
        ])
    })
])

Putting this all together

With these features in place, I can now define complex workflows for agents. This process has worked well for me internally, including in building ctx.traits itself. It is not perfect, but it is a step closer to the level of control I want.

procedure([
    sequence.prompt("implementation-planning", {
        agent: planningAgent,
        input: [productSpec, planGuidelines],
        text: prompt.text`Create a detailed implementation plan for: ${featureRequest}`,
        output: implementationPlan,
    }),

    sequence.prompt("implementation-work", {
        agent: workerAgent,
        input: [codeStandards, codeConventions],
        text: prompt.text`Implement the change in: ${implementationPlan}.`,
        output: implementationWorkReport,
    }),

    sequence.loop("implementation-refinement-loop", [
        sequence.prompt("implementation-review", {
            agent: reviewAgent,
            input: [implementationPlan, reviewGuidelines],
            text: prompt.text`Review the implemented work report: ${implementationWorkReport}`,
            output: reviewVerdict,
        }),
        sequence.prompt("implementation-refinement-work", {
            agent: workerAgent,
            input: [codeStandards, codeConventions],
            text: prompt.text`Apply the reviewer findings for the change: ${reviewVerdict}.`,
            output: implementationWorkReport,
        }),
    ], { until: condition.equals(reviewVerdict, schema.Verdict.Approved) }),

    sequence.prompt("commit-message-writing", {
        agent: scribeAgent,
        input: [implementationPlan, implementationWorkReport],
        text: "Write the commit message for implemented work.",
        output: commitMessage,
    }),

    sequence.command("git-stage", {
        title: "Stage all changes for commit",
        text: "git add -A",
    }),

    sequence.command("git-commit", {
        title: "Stage all changes for commit",
        text: command.text`git commit "${commitMessage}"`,
    }),
]);

Fine-tuning the agents

A trait is a shareable contract that can be used either by an individual across their projects or by team members across different products or within a product’s areas.

Configuration, on the other hand, is something I have decided to decouple. This is a choice made depending on where it runs or who runs it.

schema-version = "0.1.0"

[agent]
harness = "opencode"
model = "opencode-go/deepseek-v4-pro"
session-mode = "persistent"

[agent.narrator]
harness = "opencode"
model = "opencode-go/deepseek-v4-flash"
session-mode = "persistent"

[agent.role.scribe]
harness = "opencode"
model = "opencode-go/deepseek-v4-flash"

[agent.role.worker]
harness = "opencode"
model = "openai/gpt-5.6-terra-fast"
reasoning-effort = "medium"

[agent.role.reviewer]
harness = "claude-code"
model = "claude-fable-5"
reasoning-effort = "xhigh"

With this, I arrived at a solution that brings these patterns together declaratively, while keeping full control of the outer loop. This is not the end. After spending time creating traits with ctx.traits, I realize there is still much to explore.

Graph · Connecting the dots #

I understand graphs from the algebraic and the programming side. Their benefits are tremendous, being the natural language for relationships, reachability, and parallelism, and most of the problems we solve are built on them.

I have been following the recent discussions about graph engineering on x, looking for genuinely new approaches to agentic problems. So far, most of what I have seen are ideas I have already addressed using the concepts above. Much of it feels like a rebranding of familiar patterns, dressed up with new terminology.

  • Backward edge that routes failed work to a revise step is a loop. - Branch taken on a reviewer’s verdict is a conditional. - Fan-out to parallel workers with a join at the end is a parallel block.

So far, I have felt that adopting this approach would undermine the ability to validate how a procedure is modeled. I am not convinced by the idea of arbitrary control flow, where any node can route to any other and the topology can change during execution. Workflows built this way are nearly impossible to review before they run and difficult to reproduce afterward.

Still, I see value in applying graphs to agentic workflows, though not as a mechanism for control flow. This kind of connectedness has a place in specific areas, for certain types of tasks and behaviors.

The first example I have been exploring is a session abstraction that enables deterministic sharing of memory between agents, rather than isolating memory per agent or per step.

This forms a graph of knowledge of who knows what, and when. Unlike arbitrary control flow, it makes runs easier to reason about. I am actively prototyping this and working to find the best structure, with the goal of integrating it into ctx.traits soon.

Composition · Rhythm of reuse #

One area I wanted to address in ctx.traits is how to reuse proven patterns with Agents, while maintaining confidence that the Agent can reliably repeat the process.

While it is possible to create Skills and compose them, relying on Harness to select the right skills, or having to remember the exact sequence myself, feels inefficient and unnecessarily tedious.

Having seen the power of composition in functional programming, I set out to design a system where both traits and procedures can be composed in a meaningful way, making them easy to extract and reuse.

sequence.linear("implement-flow", [
    sequence.prompt("implement-work", {
        agent: worker,
        text: prompt.text`Implement feature based on specification: ${featureSpec}`,
        input: [implementReviewerdictA, implementReviewerdictB],
        output: [implementWorkBrief],
    }),
    sequence.prompt("implement-review-a", {
        agent: reviewerA,
        text: prompt.text`Review ${implementWorkBrief}, against spec ${featureSpec}`,
        output: [implementReviewVerdictA],
    }),
    sequence.prompt("plan-review-b", {
        agent: reviewerB,
        text: prompt.text`Review ${implementWorkBrief}, against spec ${featureSpec}`,
        output: [implementReviewVerdictB],
    }),
]),

I noticed I was writing this sequence in several places, each time with minor variations. While implementation work and planning work are not identical, the outer procedure is nearly the same. Since sequence serves as an abstraction over orchestrating agents toward a goal, it can be extracted into a function.

const reviewedWorkflow = (settings) => sequence.linear(/* ... */);

This approach allows us to reuse that workflow pattern for different goals, keeping the structure consistent across them.

const planWorkflow = reviewedWorkflow({ id: "plan", output: planBrief, /* ... */ });
const workWorkflow = reviewedWorkflow({ id: "work", output: workBrief, /* ... */ });
const testWorkflow = reviewedWorkflow({ id: "test", output: testBrief, /* ... */ });

export default trait("implement", {
    /* ... */
    procedure: procedure({
        /* ... */
        sequence: [planWorkflow, workWorkflow, testWorkflow],
    }),
});

With this, we can manage different agentic workflows using reusable processes, avoiding the need to copy-paste prompts or manually orchestrate agents. How far this is taken depends on the user’s needs; it can be extracted into a team or company library, used across projects, or shared publicly.

Open Problems · Energy drainage #

Even though these patterns, with the right tools, came together into something I enjoyed using, the process still felt wasteful, especially when moving between projects.

I found that while I had solved the problem of authoring and running workflows for myself, reusing them elsewhere required copying and pasting files or procedures. This brought back the same problems I had with skills: keeping everything in sync became a chore.

Dependency Management

A skill rarely lives alone, especially when used within a team or written in a useful, generalized form. With traits, I wanted to make this process much easier, starting with how traits are shared.

For now, I have settled on using the npm ecosystem. Since traits are written as code, it is straightforward to install a package and import it in your .ts

files.

import { resources } from "@ctx-company/useful-traits";

..

// Import the same shared standards and review rubric.
// No need to copy-paste the files, just import them.
sequence.prompt("Implementation Review", {
    text: "Review the implementation.",
    input: [resources.codingStandards, resources.reviewRubric],
})

..

// Additionally, you have full control where resource is in your prompt.
sequence.prompt("Implementation Review", {
    text: prompt.text`Review the implementation, grade based on ${resources.reviewRubric}.`,
    input: [resources.codingStandards],
})

When traits are imported at build time, the sync step resolves and vendors them, writing a lockfile. This ensures there is no reliance on npm’s mechanics to keep the imported package from changing, whether it is pulled in by the author or by an update command.

I am still figuring out how much to rely on the npm ecosystem. In the future, I may want to reduce that reliance, but I prefer not to make assumptions until I see how people actually use it.

Versioning

Once traits become dependencies, they need versions, and more importantly, a way to prove which version you and your team are actually running. This guarantee lets you trust that a reviewed trait stays the same, which is especially important when working with non-deterministic agents.

This is solved with both lockfiles that map to the exact source revisions of dependencies and checksums tied to the exact built artifacts of traits.

[[dependency]]
name = "@ctx-company/useful-traits"
version = "1.2.3"
resolved = "sha256:9f0d.." # checksum of the dependency's source

[trait]
name = "deep-refactor"
version = "2.0.1"
checksum = "sha256:a52d.." # checksum of the trait's built artifact

As with dependencies, this design choice is still evolving. I expect to revisit it as the ecosystem matures.

Composition

I was initially skeptical about this part, but it has become the one I enjoy most. Being able to reuse parts of traits across the ecosystem is useful, and I am interested to see how it develops.

For example, a shared coding-standards trait can be used across your team without needing to reference it in every project’s skill file. Agents can also be reused as primed reviewers or workers, without redefining them in each trait.

Right now, it mostly works by taking advantage of the fact that traits are code, so you can import them as usual in your .ts

files.

import { resources, agents } from "@ctx-company/useful-traits";

..

export default trait("deep-refactor", {
    agent: agents.reviewer,
    resources: resources.codingStandards,
});

In the near future, I aim to make it possible to import traits as sub-procedures. This should allow workflow parts to be composed more naturally.

Bi-Directional Compatibility

I want the ecosystem to be as compatible as possible with existing agents, so I have decided to support importing and exporting traits from and to SKILL.md and AGENTS.md. This should make it easier to bridge between existing and new systems.

$ ctx traits import ./SKILL.md

$ ctx traits render deep-refactor --profile agent-skills

This will be improved in the near future. Supporting MCP-based communication is a priority to bring traits beyond the terminal.

Conclusion · It’s not done yet #

Right now, I am aware that ctx.traits has become overloaded with scope and needs to be split into separate projects. This over was a deliberate choice. I want to discover how much belongs inside ctx.traits and how much should be extracted.

This project is only the first layer of a longer-term roadmap. I am planning something larger as part of my new company, with the aim of making AI-driven work genuinely enjoyable and productive.

If any of this resonates with you, please subscribe to my newsletter below and follow me on X. I’ll be sharing my thoughts and documenting my journey along the way.

Thank you for reading!

── more in #ai-agents 4 stories · sorted by recency
── more on @ctx.traits 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/show-hn-ctx-traits-a…] indexed:0 read:20min 2026-07-29 ·